今回は、データベース内に保存された文章のテキストデータから単語の出現頻度を使って話題になっているトピックを抽出する、という処理を行ってみます。
- テキストを形態素解析する
- 形態素解析した結果をJSONBで取得する
- JSONBデータを対象に集計処理を行う
- 上記すべてをサーバサイドで実行する
■データの準備
今回も東京カレンダーの「東京女子図鑑」からの文章をサンプルとして使ってみます。
- 31歳女性がするべき、銀座での“上質な”暮らし。大人の女の流儀とは?(1/2)[東京カレンダー]
https://tokyo-calendar.jp/article/4640
snaga=# \d docs Table "public.docs" Column | Type | Modifiers ---------+---------+----------- docid | integer | not null doctext | text | Indexes: "docs_pkey" PRIMARY KEY, btree (docid) snaga=# SELECT docid, length(doctext) FROM docs; docid | length -------+-------- 1 | 4423 (1 row) snaga=# SELECT docid, substring(doctext,0,60) AS doctext FROM docs; docid | doctext -------+--------------------------------------------------------------------------------------------------------------------- 1 | 20代後半頃から、同期が1人また1人と、会社を辞めていきました。辞める理由はいろいろありますが、病んでしまった子もいれ (1 row) snaga=#
■形態素解析を行うユーザ定義関数を作成する
まず、テキストを入力として受け取り、形態素解析した結果をJSONB型として返却するユーザ定義関数を作成します。
形態素解析には MeCab を、ユーザ定義関数には PL/Python を、PL/Python と MeCab のバインディングには Mecab-Python を使います。環境は以前のエントリと同様ですので、そちらを参考に準備してください。PostgreSQLのバージョンは9.5です。
- 自動要約API「summpy」を使ってPostgreSQLに文章の要約機能を追加する
http://pgsqldeepdive.blogspot.jp/2015/11/pgsummpy.html
CREATE OR REPLACE FUNCTION mecab_tokenize_jsonb(string text) RETURNS SETOF jsonb AS $$ import MeCab import json import plpy import sys def mecab_text2array(string): a = [] m = MeCab.Tagger("-Ochasen") """ Mecabに渡すためにはunicodeではなくutf-8である必要がある。 Mecabから戻ってきたらunicodeに戻す。 また、Mecabはエンコード済みのutf-8文字列へのポインタを返すので、 on-the-flyでutf-8に変換するのではなく、変数として保持しておく 必要がある。(でないとメモリ領域がGCで回収されてデータが壊れる) 参照: http://shogo82148.github.io/blog/2012/12/15/mecab-python/ """ enc_string = string node = m.parseToNode(enc_string) while node: n = {} n['surface'] = node.surface.decode('utf-8') n['feature'] = node.feature.decode('utf-8').split(",") n['cost'] = node.cost a.append(n) node = node.next return a for w in mecab_text2array(string): yield(json.dumps(w)) $$ LANGUAGE plpythonu;ポイントは
- MeCabの持っているデータ型をそのままJSONエレメントとして返却。
- 「surface」はトークンにした文字列、「feature」はその文字列の属性情報。
- jsonb型を行で返却する関数として定義。
■文章を品詞に分解する
それでは doctext カラムに保存されているテキストを形態素解析してみます。
SELECT docid, mecab_tokenize_jsonb(doctext) FROM docs;mecab_tokenize_jsonb()の引数に doctext カラムを渡してSELECT文を実行すると、以下のようにトークンごとの情報が返却され、品詞の情報をJSONBで取得できるようになります。
docid | mecab_tokenize_jsonb -------+----------------------------------------------------------------------------------------------------------------------------------------------------------- 1 | {"cost": 0, "feature": ["BOS/EOS", "*", "*", "*", "*", "*", "*", "*", "*"], "surface": ""} 1 | {"cost": 27956, "feature": ["名詞", "数", "*", "*", "*", "*", "*"], "surface": "20"} 1 | {"cost": 28475, "feature": ["名詞", "接尾", "助数詞", "*", "*", "*", "代", "ダイ", "ダイ"], "surface": "代"} 1 | {"cost": 33141, "feature": ["名詞", "副詞可能", "*", "*", "*", "*", "後半", "コウハン", "コーハン"], "surface": "後半"} 1 | {"cost": 38173, "feature": ["名詞", "接尾", "副詞可能", "*", "*", "*", "頃", "ゴロ", "ゴロ"], "surface": "頃"} 1 | {"cost": 37905, "feature": ["助詞", "格助詞", "一般", "*", "*", "*", "から", "カラ", "カラ"], "surface": "から"} (...) 1 | {"cost": 4436512, "feature": ["名詞", "代名詞", "一般", "*", "*", "*", "彼", "カレ", "カレ"], "surface": "彼"} 1 | {"cost": 4441497, "feature": ["助詞", "並立助詞", "*", "*", "*", "*", "や", "ヤ", "ヤ"], "surface": "や"} 1 | {"cost": 4436788, "feature": ["記号", "読点", "*", "*", "*", "*", "、", "、", "、"], "surface": "、"} 1 | {"cost": 4440439, "feature": ["名詞", "一般", "*", "*", "*", "*", "上司", "ジョウシ", "ジョーシ"], "surface": "上司"} 1 | {"cost": 4440813, "feature": ["助詞", "連体化", "*", "*", "*", "*", "の", "ノ", "ノ"], "surface": "の"} 1 | {"cost": 4443900, "feature": ["名詞", "一般", "*", "*", "*", "*", "おかげ", "オカゲ", "オカゲ"], "surface": "おかげ"} 1 | {"cost": 4444601, "feature": ["助詞", "格助詞", "一般", "*", "*", "*", "で", "デ", "デ"], "surface": "で"} (...) 1 | {"cost": 4543115, "feature": ["動詞", "自立", "*", "*", "五段・ワ行促音便", "連用形", "思う", "オモイ", "オモイ"], "surface": "思い"} 1 | {"cost": 4540257, "feature": ["助動詞", "*", "*", "*", "特殊・マス", "基本形", "ます", "マス", "マス"], "surface": "ます"} 1 | {"cost": 4537422, "feature": ["記号", "句点", "*", "*", "*", "*", "。", "。", "。"], "surface": "。"} 1 | {"cost": 4535886, "feature": ["BOS/EOS", "*", "*", "*", "*", "*", "*", "*", "*"], "surface": ""} (2718 rows)
■単語ごとの出現頻度を集計する
それでは、ここまでの結果を使って単語ごとの出現頻度を集計してみます。
JSONBのデータからキーを指定してエレメントを取り出すには「->」を使います。
json_obj->'key_name'
この場合はエレメントがJSONB型で返却されます(JSONBの内部に再帰的にアクセスしたい場合)。text型で取り出したい場合には「->>」を使います。
json_obj->>'key_name'
また、配列の要素へアクセスしたい場合には、「->0」のように添字で指定することで取得することができます。
json_obj->'array_name'->>0
詳細は以下のマニュアルを参照してください。
- 9.15. JSON関数と演算子
https://www.postgresql.jp/document/9.5/html/functions-json.html
よって、JSONBからトークンと品詞種別を取り出し、この2つをGROUP BYのキーとしてCOUNTで出現頻度の集計をしてみます。
WITH doc AS ( SELECT docid, mecab_tokenize_jsonb(doctext) t FROM docs ) SELECT docid, t->>'surface' as surface, t->'feature'->>0 as feature, count(*) FROM doc GROUP BY docid, t->>'surface', t->'feature'->>0 ORDER BY 4 DESC;
このクエリを実行して出現頻度順にトークンを並べると、以下のような結果が得られます。
docid | surface | feature | count -------+------------------------------+---------+------- 1 | 、 | 記号 | 175 1 | の | 助詞 | 133 1 | て | 助詞 | 104 1 | 。 | 記号 | 103 1 | に | 助詞 | 81 1 | た | 助動詞 | 73 1 | が | 助詞 | 65 (...) 1 | の | 名詞 | 14 1 | 銀座 | 名詞 | 14 1 | 私 | 名詞 | 13 1 | なっ | 動詞 | 13 1 | って | 助詞 | 13 (...) 1 | 感覚 | 名詞 | 1 1 | 食べ | 動詞 | 1 1 | あぁ | 感動詞 | 1 1 | 出す | 動詞 | 1 1 | 詰まっ | 動詞 | 1 (787 rows)
■出現している品詞の種類を調べる
先に集計したランキングを見ると、当たり前ですが記号や助詞の出現頻度が高く、このままではあまり面白い結果になりません。そのため、簡単な方法として品詞の種別でフィルタリングすることを考えてみます。
まずは、出現している品詞の種別の一覧を確認してみましょう。
WITH doc AS ( SELECT docid, mecab_tokenize_jsonb(doctext) t FROM docs ) SELECT DISTINCT t->'feature'->>0 as feature FROM doc
品詞種別の一覧を見てみると、12種類の品詞が出てきているようです。
feature --------- 助動詞 名詞 接頭詞 助詞 形容詞 副詞 連体詞 動詞 感動詞 BOS/EOS 接続詞 記号 (12 rows)
■品詞の種別を絞ってランキングを作成する
それでは、最後に「名詞」に絞って表示してみましょう。
以下のクエリでは名詞に絞った上で、ひらがな1文字または2文字の結果も省いています。
WITH doc AS ( SELECT docid, mecab_tokenize_jsonb(doctext) t FROM docs ) SELECT docid, t->>'surface' as surface, t->'feature'->>0 as feature, count(*) FROM doc WHERE t->'feature'->>0 IN ('名詞') AND regexp_replace(t->>'surface', '^[あいうえおかきくけこさしすせそたちつてとなにぬねのはひふへほまみむめもやゆよわをんがぎぐげござじずぜぞだぢづでどばびぶべぼぱぴぷぺぽっゃゅょ]{1,2}$', '') <> '' GROUP BY docid, t->>'surface', t->'feature'->>0 ORDER BY 4 DESC;
このクエリを実行すると、以下の結果が得られます。
docid | surface | feature | count -------+------------------------------+---------+------- 1 | 方 | 名詞 | 15 1 | 銀座 | 名詞 | 14 1 | 私 | 名詞 | 13 1 | 人 | 名詞 | 11 1 | 上司 | 名詞 | 10 1 | 彼 | 名詞 | 9 1 | 歳 | 名詞 | 8 1 | 女性 | 名詞 | 8 1 | 恵比寿 | 名詞 | 7 1 | 店 | 名詞 | 7 1 | 笑 | 名詞 | 7 1 | 同期 | 名詞 | 6 1 | 大人 | 名詞 | 6 1 | 女 | 名詞 | 6 (...) 1 | 最中 | 名詞 | 3 1 | 年上 | 名詞 | 3 1 | 桟敷 | 名詞 | 3 1 | 外資 | 名詞 | 3 1 | 1 | 名詞 | 3 1 | 理由 | 名詞 | 3 1 | 箱 | 名詞 | 3 1 | 前 | 名詞 | 3 1 | 秋田 | 名詞 | 3 (...) 1 | 子供 | 名詞 | 1 1 | 赤 | 名詞 | 1 1 | 幕間 | 名詞 | 1 1 | 確か | 名詞 | 1 1 | ビーフ | 名詞 | 1 1 | 700 | 名詞 | 1 (433 rows)
このラインキングを見ると、どのようなトピックがどの程度話題になっているかを、おおまかに見てとることができます。
例えば、
- 恵比寿より銀座の方が言及が多い(恵比寿から銀座に引っ越した話なので当然と言えば当然)
- 彼より上司の方が言及が多い
- 誰よりも自分自身(私)の話が多い
■まとめ
というわけで、今回はPostgreSQL内部に保存したテキストに自然言語処理を適用して、言及されている話題を抽出してみる方法を簡単にご紹介しました。
データベース内に保存されている数値データやコードなどの分析に加えて、自然言語処理によって通常の文章から情報を抽出できるようになると、データ分析がさらに楽しくなると思います。
より技術的に進んだ手法も応用できる領域もいろいろあると思いますので、興味のある方はぜひチャレンジしてみていただければと思います。
では、また。
0 件のコメント:
コメントを投稿