LoginSignup
2
1

More than 1 year has passed since last update.

CentOS7 + Python3 + Mecab + Doc2VecでWikipediaのデータを解析した記録

Last updated at Posted at 2022-01-23

本稿では、ウィキペディアの各単語に対する類似単語ベスト10を、ウィキペディアから抽出した記録を可能な限り詳しく書きました。

目次

1.はじめに
2.本稿の目的
3.作業環境
4.最新のウィキペディアデータの取得
5.テキストデータを抽出
6.pageとcategorylinksのデータをmysqlデータベースにインポート
7.mecabの辞書に単語を追加
8.テキストデータをmysqlデータベースに登録
9.doc2vecを用いた機械学習で類似単語を抽出
10.目視での結果評価
11.後処理
12.スケジュール化
13.参考サイト
14.まとめ

1. はじめに

こんにちは。アラカンの現役プログラマです。プログラマ歴は長いのですが、頭の中が古いため、少しでも知識をアップデートする目的で、2021年8月から5か月余り、機械学習の一分野である自然言語処理について学んでいます。機械学習やPythonについては初心者のため、お気づきの点はご指摘いただければ幸いです。

2. 本稿の目的

 前述の通り、現在自然言語処理を学習中で、そのアウトプットとして投稿します。より具体的な目的は、概ね下記の通りです。
・解析作業の過程で、Pythonによるコーディングに少しでもなれること
・Wikipediaの単語間の類似性という極めて個人的な興味を満たすこと
・投稿前に発生する作業で得られた解析結果を、個人的な業務に生かすこと
尚、「単語間の類似性」は、やや恣意的ではありますが、「単語の説明文が似ていること」と定義します。
以下、類似性としての最終的な出力形式です。

単語,類似単語1,コサイン類似度1,類似単語2,コサイン類似度2,類似単語3,コサイン類似度3,.....,類似単語10,コサイン類似度10

3. 作業環境

 ハードウェア環境(会社のPC)は下記の通りです。
CPU: Intel(R) Xeon(R) Gold 5218 CPU @ 2.30GHz × 16コア32スレッド
メモリ: 実メモリ64GB、スワップ72GB
SSD: 1TB
ソフトウェア環境は表題にある通り、基本的にCentOS7 + Python3 + Mecab + Doc2Vecですが、Wikipediaの実データや機械学習の結果を一時的に保存する目的でMysql、Wikipediaデータからタグを除去して純粋に近いテキストに変換するためのツールであるwk2txt等も採用しました。
環境構築作業についても、本稿と同じぐらいのボリュームの記事になるほどの苦労がありましたが、内容の方向性が異なるため割愛します。
以下の章では、実作業について順次記述していきます。

4. 最新のウィキペディアデータの取得

 はじめに、解析対象のデータである直近のWikipediaデータのダンプ(最小限)をダウンロードしました。一般に直近のWikipediaデータのダンプは、https://dumps.wikimedia.org/jawiki/latest/
以下に格納されています。下記コマンドでダウンロードできました。

mkdir ~/workspace/jawiki_latest
cd ~/workspace/jawiki_latest
wget https://dumps.wikimedia.org/jawiki/latest/jawiki-latest-pages-articles.xml.bz2 --no-check-certificate
wget https://dumps.wikimedia.org/jawiki/latest/jawiki-latest-page.sql.gz --no-check-certificate
wget https://dumps.wikimedia.org/jawiki/latest/jawiki-latest-categorylinks.sql.gz --no-check-certificate

5. テキストデータを抽出

wk2txtを利用して、下記の通りテキストデータを抽出し、最終的に拡張子txtのテキストデータに変換されました。

/usr/local/rbenv/bin/rbenv exec wp2txt --input-file jawiki-latest-pages-articles.xml.bz2

出力されたファイル(本稿執筆時点):
jawiki-latest-pages-articles.xml-001.txt~jawiki-latest-pages-articles.xml-659.txt

6. pageとcategorylinksのデータをmysqlデータベースにインポート

mysqlのターミナルにログイン後、下記のコマンドを実行。

create database jawiki_latest CHARACTER SET utf8mb4 COLLATE utf8mb4_bin;

ターミナルからログアウト後、下記のコマンドでインポートできました。

mysql -uユーザ名 jawiki_latest < jawiki-latest-page.sql
mysql -uユーザ名 jawiki_latest < jawiki-latest-categorylinks.sql

7. mecabの辞書に単語を追加

この解析作業では、mecabの辞書にWikipediaの単語を登録することが重要となります。
デフォルトの状態では下記のように分かち書きされますが、

$ mecab -Owakati
東京オリンピック2020は、2021年に開催された。
東京 オリンピック 2020 は 、 2021 年 に 開催 さ れ た 。

下記の通り、Wikipediaの単語を反映した形で分かち書きされなければなりません。

$ mecab -Owakati
東京オリンピック2020は、2021年に開催された。
東京オリンピック2020 は 、 2021年 に 開催 された 。

この目的を達成するために、以下の手順で、mecabの辞書にWikipediaの単語を登録しました。
【手順1】Wikipediaの単語を/tmp/jawiki_page.csvに保存

mysql -u mysql jawiki_latest -e "SELECT p.page_title page_title FROM page p where page_title <> '\"\'' GROUP BY p.page_title" | perl -pe 's;,;、;g' | perl -pe 's;\t;,;g' > /tmp/jawiki_page.csv

【手順2】mecab辞書の最終形の前段階のcsvを作成するために、以下のコードを作成して保存

parse.py
# encoding: utf-8
import codecs
import os
def isValid(word):
        try:
                if len(word) == 1 or u'_' in word:
                        return False
                return True
        except:
                return False
def parse(infile,output_file):
        fout = codecs.open(output_file , "a+", "utf-8")
        fin = codecs.open(infile, "r", "utf-8")
        dup_map = {}
        for line in fin:
                word = line.rstrip().split(",")[0]
                if "page" in word:
                        continue
                if isValid(word) and word not in dup_map:
                        dup_map[word] = True
                        cost = int(max(-36000, -400 * len(word)**1.5))
                        fout.write(u"%s,-1,-1,%d,名詞,一般,*,*,*,*,*,*,wikipedia\n" %(word, cost))
        fin.close()
        fout.close()
os.system("rm -f /tmp/wikiword.csv")
parse("/tmp/jawiki_page.csv", "/tmp/wikiword.csv")

【手順3】parse.pyを実行し、mecab辞書の最終形の前段階のcsvである、/tmp/wikiword.csvを作成。

/opt/rh/rh-python38/root/usr/bin/python3 parse.py

【手順4】/tmp/wikiword.csvを、mecab辞書の形式に変換。

/usr/libexec/mecab/mecab-dict-index -d /usr/lib64/mecab/dic/ipadic -u /tmp/user_dic.dic -f utf-8 -t utf-8 /tmp/wikiword.csv

【手順5】手順4で作成した辞書を、mecabが利用できるように設定。

bash -c 'echo "userdic = /tmp/user_dic.dic" >> /etc/mecabrc'

※手順1~手順5の一連の内容については、下記のサイトをほぼそのまま参考にさせていただきました。
https://techblog.gmo-ap.jp/2020/11/10/mecab_wikipedia_dictionary/
このサイトの作者の方にはこの場を借りて深謝します。

8. テキストデータをmysqlデータベースに登録

5章でwk2txtを利用して抽出したWikipediaのテキストデータを扱いやすくするために、データベースに登録します。
現時点で、jawiki-latest-pages-articles.xml-001.txt~jawiki-latest-pages-articles.xml-659.txtとしてテキストファイル化されているデータは、下記のような形式になっています。
[[単語1]]
・・・・・・・・・・・・・・・・
・・・・・・・・・・・・・・・・
・・・・・・・・・・・・・・・・・
==項目11==
・・・・・・・・・・・・・・・
・・・・・・・・・・
==項目12==
・・・・・・

[[単語2]]
・・・・・・・・・・・
・・・・・・・・・・・・
==項目21==
・・・・・・・・・・

このような規則性に注目して、テキストファイルをスキャンし、順次データベースのテーブルに登録します。
まずは、mysqlのターミナルにログインし、下記のSQLを実行してテーブルを作成します。

CREATE TABLE `wikipedia_textinfo` (
    ->   `id` int(11) NOT NULL AUTO_INCREMENT,
    ->   `page_title` varbinary(255) DEFAULT NULL,
    ->   `item` text,
    ->   `content` text,
    ->   PRIMARY KEY (`id`),
    ->   KEY `ttxt` (`page_title`)
    -> ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

その後、下記のプログラムを作成します。

register_textinfo.py
# -*- coding: utf-8 -*-

# osのインポート
import os
# reのインポート
import re
# MySQLdbのインポート
import MySQLdb

# Wikipediaデータの日付を定義
WIKIPEDIA_YYYYMMDD = 'latest'
# bulk insertの単位を定義
BULK_INSERT_UNIT = 10000
# insertのSQLを定義
INSERT_SQL = "INSERT INTO wikipedia_textinfo (page_title, item, content) VALUES (%s,%s,%s)"

# データベースへの接続とカーソルの生成
connection = MySQLdb.connect(
    host='localhost',
    user='ユーザ名',
    passwd='パスワード',
    db='jawiki_latest',
# テーブル内部で日本語を扱うために追加
    charset='utf8mb4'
)
cursor = connection.cursor()

# txtファイルからテキストデータを抽出して、wikipedia_textinfoに登録する
file_id = 1
str_file_id = format(file_id, '03')
txt_file_name = 'jawiki-' + WIKIPEDIA_YYYYMMDD + '-pages-articles.xml-' + str_file_id + '.txt'
bulk_insert_count = 0
bulk_insert_arr = []
while os.path.exists(txt_file_name):
    print('processing ' + txt_file_name + '...')
    now_title = ''
    now_item = ''
    with open(txt_file_name) as f:
        while s_line := f.readline():
            s_line = s_line.strip()
            if reg_result := re.match("^\[\[([^\[\]]+)\]\]$", s_line):
                now_title = reg_result.group(1)
                now_item = ''
                continue
            if reg_result := re.match("^[\=]+[\ ]*([^\ \=]+)[\ ]*[\=]+$", s_line):
                now_item = reg_result.group(1)
                continue

            now_content = s_line.replace("[[", "")
            now_content = now_content.replace("]]", "")
            if '|' in now_content or '。' not in now_content or len(now_content) == 0:
                continue;
            bulk_insert_count += 1
            bulk_insert_arr.append([now_title, now_item, now_content])
            if bulk_insert_count >= BULK_INSERT_UNIT:
                cursor = connection.cursor()
                cursor.execute("SET NAMES utf8mb4")
                cursor.executemany(INSERT_SQL, bulk_insert_arr)
                connection.commit()
                bulk_insert_count = 0
                bulk_insert_arr = []
    file_id += 1
    str_file_id = format(file_id, '03')
    txt_file_name = 'jawiki-' + WIKIPEDIA_YYYYMMDD + '-pages-articles.xml-' + str_file_id + '.txt'

if bulk_insert_count > 0:
    cursor = connection.cursor()
    cursor.execute("SET NAMES utf8mb4")
    cursor.executemany(INSERT_SQL, bulk_insert_arr)
    connection.commit()

# 接続を閉じる
connection.close()

その後、このプログラムを実行することにより、データベースにテキストデータが登録されます。

/opt/rh/rh-python38/root/usr/bin/python3 register_textinfo.py

9. doc2vecを用いた機械学習で類似単語を抽出

【手順0】結果を格納するDatabaseのテーブルを定義します。尚、冒頭で述べた通り、結果は下記のような形で格納されます。
単語,類似単語1,コサイン類似度1,類似単語2,コサイン類似度2,類似単語3,コサイン類似度3,.....,類似単語10,コサイン類似度10

また、結果は下記の2通りのパターンで抽出します。
・doc2vecのみで抽出するパターン
・doc2で抽出した類似単語を、カテゴリ縛り(同一カテゴリに属していない単語は類似単語とみなさない)でフィルタリングするパターン

したがって、実際のテーブル定義情報は、下記の通りとしました。

CREATE TABLE `wikipedia_similar_word` (
  `id` int(11) NOT NULL AUTO_INCREMENT,
  `word` varbinary(255) DEFAULT NULL,
  `similar_word1` varbinary(255) DEFAULT NULL,
  `cosine_similarity1` double DEFAULT NULL,
  `similar_word2` varbinary(255) DEFAULT NULL,
  `cosine_similarity2` double DEFAULT NULL,
  `similar_word3` varbinary(255) DEFAULT NULL,
  `cosine_similarity3` double DEFAULT NULL,
  `similar_word4` varbinary(255) DEFAULT NULL,
  `cosine_similarity4` double DEFAULT NULL,
  `similar_word5` varbinary(255) DEFAULT NULL,
  `cosine_similarity5` double DEFAULT NULL,
  `similar_word6` varbinary(255) DEFAULT NULL,
  `cosine_similarity6` double DEFAULT NULL,
  `similar_word7` varbinary(255) DEFAULT NULL,
  `cosine_similarity7` double DEFAULT NULL,
  `similar_word8` varbinary(255) DEFAULT NULL,
  `cosine_similarity8` double DEFAULT NULL,
  `similar_word9` varbinary(255) DEFAULT NULL,
  `cosine_similarity9` double DEFAULT NULL,
  `similar_word10` varbinary(255) DEFAULT NULL,
  `cosine_similarity10` double DEFAULT NULL,
  PRIMARY KEY (`id`),
  KEY `tword` (`word`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4

CREATE TABLE `wikipedia_similar_word_category` (
  `id` int(11) NOT NULL AUTO_INCREMENT,
  `word` varbinary(255) DEFAULT NULL,
  `similar_word1` varbinary(255) DEFAULT NULL,
  `cosine_similarity1` double DEFAULT NULL,
  `similar_word2` varbinary(255) DEFAULT NULL,
  `cosine_similarity2` double DEFAULT NULL,
  `similar_word3` varbinary(255) DEFAULT NULL,
  `cosine_similarity3` double DEFAULT NULL,
  `similar_word4` varbinary(255) DEFAULT NULL,
  `cosine_similarity4` double DEFAULT NULL,
  `similar_word5` varbinary(255) DEFAULT NULL,
  `cosine_similarity5` double DEFAULT NULL,
  `similar_word6` varbinary(255) DEFAULT NULL,
  `cosine_similarity6` double DEFAULT NULL,
  `similar_word7` varbinary(255) DEFAULT NULL,
  `cosine_similarity7` double DEFAULT NULL,
  `similar_word8` varbinary(255) DEFAULT NULL,
  `cosine_similarity8` double DEFAULT NULL,
  `similar_word9` varbinary(255) DEFAULT NULL,
  `cosine_similarity9` double DEFAULT NULL,
  `similar_word10` varbinary(255) DEFAULT NULL,
  `cosine_similarity10` double DEFAULT NULL,
  PRIMARY KEY (`id`),
  KEY `tword` (`word`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4

 この章の内容は、一つのプログラムのみで記述されていますが、本稿で最も重要な部分ですので、順を追って処理の内容を説明します。
【手順1】mysqlサーバーに接続します。

# データベースへの接続とカーソルの生成
connection = MySQLdb.connect(
    host='localhost',
    user='ユーザ名',
    passwd='パスワード',
    db='jawiki_latest',
# テーブル内部で日本語を扱うために追加
    charset='utf8mb4'
)
cursor = connection.cursor()
cursor.execute("SET NAMES utf8mb4")

【手順2】Wikipediaの単語をすべて取得し、下記の通り変数を初期化します。
arr_page_title = [単語1, 単語2, 単語3,........]
dict_page_content = {単語1:'', 単語2:'', 単語3:'', ...........}

# Wikipedia語を全て取得
query = "select distinct page_title from wikipedia_textinfo"
cursor.execute(query)

arr_page_title = []
dict_page_content = {}
g = 0
for row in cursor:
    s = row[0].decode('UTF-8')
    s = s.replace('_', ' ')
    arr_page_title.append(s)
    dict_page_content[s] = ''
    g += 1
    if g % 1000 == 0:
        print('g = ' + str(g))

【手順3】Wikipediaの単語毎の説明文をすべて取得し、下記の通り変数に格納します。
dict_page_content = {単語1:説明文1, 単語2:説明文2, 単語3:説明文3, ...........}

# Wikipedia語の説明文を全て取得し、dict_page_contentに格納
query = "select page_title, content from wikipedia_textinfo page_title order by id"
cursor.execute(query)
h = 0
for row in cursor:
    dict_page_content[row[0].decode('UTF-8')] += row[1]
    h += 1
    if h % 1000 == 0:
        print('h = ' + str(h))

【手順4】すべての単語の説明文を、mecabで分かち書きし、変数training_docsに格納します。

# Mecabで、全ての説明文を分かち書きし、順にtraining_docsに格納
training_docs = []
token = []
i = 0
page_title = []
for k, v in dict_page_content.items():
    if len(v) > 0:
        token.append(tokenizer(v))
        page_title.append(k)
        training_docs.append(TaggedDocument(words=token[i], tags=[page_title[i]]))
        i += 1
        if i % 1000 == 0:
            print('i = ' + str(i))

関数tokenizer(text)は、説明文を分かち書きし、下記のような配列を返します。
[単語1, 単語2, 単語3, ..........]

【手順5】全ての単語の全ての説明文が格納された配列training_docsを基に機械学習を行い、実際に結果を求めます。
結果は下記の変数に格納されます。
・doc2vecのみで抽出するパターン→result
・doc2で抽出した類似単語を、カテゴリ縛り(同一カテゴリに属していない単語は類似単語とみなさない)でフィルタリングするパターン→result_category

# 以下、training_docsのデータを学習し、各Wikipedia語と似たWikipedia語のベスト10を表示する
model = Doc2Vec(documents=training_docs, min_count=1)

# 以下、カテゴリフィルタ無しで、類似単語を求める
result = []
result_category = []
for j in range(i):
    arr_most_similar = model.dv.most_similar(page_title[j], topn=20)
    # 以下、カテゴリフィルタ無し用の処理
    csv_content = [page_title[j], '', 0.0, '', 0.0, '', 0.0, '', 0.0, '', 0.0, '', 0.0, '', 0.0, '', 0.0, '', 0.0, '', 0.0]
    for q in range(10):
        csv_content[q * 2 + 1] = arr_most_similar[q][0]
        csv_content[q * 2 + 2] = arr_most_similar[q][1]
    result.append(csv_content)
    # 以下、カテゴリフィルタ有り用の処理
    arr_most_similar_filtered = filter_by_category(connection, page_title[j], arr_most_similar)
    csv_content_category = [page_title[j], '', 0.0, '', 0.0, '', 0.0, '', 0.0, '', 0.0, '', 0.0, '', 0.0, '', 0.0, '', 0.0, '', 0.0]
    for qq in range(min([10, len(arr_most_similar_filtered)])):
        csv_content_category[qq * 2 + 1] = arr_most_similar_filtered[qq][0]
        csv_content_category[qq * 2 + 2] = arr_most_similar_filtered[qq][1]
    result_category.append(csv_content_category)
    # 以下、処理が継続中であることを確認するための表示
    if j % 1000 == 0:
        print('j = ' + str(j))

ここでfilter_by_category(connection, page_title, arr_most_similar)は、doc2vecのみで出された結果から、同一カテゴリに属さないものを外します。

【手順6】結果をCSVとデータベースに出力します。

# 以下、結果出力
output_csv(result, False)
output_database(connection, result, False)
output_csv(result_category, True)
output_database(connection, result_category, True)

文字通り、output_csvはCSVに出力する関数、output_databaseは、データベースに出力する関数です。

以下、すべての処理を記述した、プログラムの全体像です。

doc2vec_all.py
# -*- coding: utf-8 -*-

# MySQLdbとMecabのインポート
import MySQLdb
import MeCab
# Doc2Vecのインポート
from gensim.models.doc2vec import Doc2Vec
from gensim.models.doc2vec import TaggedDocument
# datetimeのインポート
import datetime
# csvのインポート
import csv
# codecsのインポート
import codecs
codecs.register_error('none', lambda e: ('', e.end))

# bulk insertの単位を定義
BULK_INSERT_UNIT = 10000
INSERT_SQL = "INSERT INTO wikipedia_similar_word (word,similar_word1,cosine_similarity1,similar_word2,cosine_similarity2,similar_word3,cosine_similarity3,similar_word4,cosine_similarity4,similar_word5,cosine_similarity5,similar_word6,cosine_similarity6,similar_word7,cosine_similarity7,similar_word8,cosine_similarity8,similar_word9,cosine_similarity9,similar_word10,cosine_similarity10) VALUES (%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s)"
INSERT_SQL_CATEGORY = "INSERT INTO wikipedia_similar_word_category (word,similar_word1,cosine_similarity1,similar_word2,cosine_similarity2,similar_word3,cosine_similarity3,similar_word4,cosine_similarity4,similar_word5,cosine_similarity5,similar_word6,cosine_similarity6,similar_word7,cosine_similarity7,similar_word8,cosine_similarity8,similar_word9,cosine_similarity9,similar_word10,cosine_similarity10) VALUES (%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s)"


def tokenizer(text):
    t = MeCab.Tagger("-Owakati")
    m = t.parse(text)
    m = t.parseToNode(text)
    tokens = []
    while m:
        tokenData = m.feature.split(",")
        token = m.surface
        tokens.append(token)
        m = m.next
    tokens.pop(0)
    tokens.pop(-1)
    return tokens

def output_csv(result, category_filter):
    if category_filter:
        fname_suffix = '_category'
    else:
        fname_suffix = ''

    with open('./wikipedia_similarity' + fname_suffix + '_utf8.csv', 'w') as f:
        writer = csv.writer(f)
        writer.writerow(['word','similar_word1','cosine_similarity1','similar_word2','cosine_similarity2','similar_word3','cosine_similarity3','similar_word4','cosine_similarity4','similar_word5','cosine_similarity5','similar_word6','cosine_similarity6','similar_word7','cosine_similarity7','similar_word8','cosine_similarity8','similar_word9','cosine_similarity9','similar_word10','cosine_similarity10'])
        for i in range(len(result)):
            csv_content = ['', '', 0.0, '', 0.0, '', 0.0, '', 0.0, '', 0.0, '', 0.0, '', 0.0, '', 0.0, '', 0.0, '', 0.0]
            for j in range(len(result[i])):
                csv_content[j] = result[i][j]
            writer.writerow(csv_content)

    uf = codecs.open('./wikipedia_similarity' + fname_suffix + '_utf8.csv', 'r', encoding = 'utf-8')
    sf = codecs.open('./wikipedia_similarity' + fname_suffix + '_sjis.csv', 'w', encoding='cp932', errors='none')
    for line in uf:
        sf.write(line)
    uf.close()
    sf.close()

def output_database(connection, result, category_filter):
    global BULK_INSERT_UNIT
    global INSERT_SQL
    global INSERT_SQL_CATEGORY

    sql = INSERT_SQL_CATEGORY if category_filter == True else INSERT_SQL

    bulk_insert_count = 0
    bulk_insert_arr = []
    for i in range(len(result)):
        similar_word_data = ['', '', '0.0', '', '0.0', '', '0.0', '', '0.0', '', '0.0', '', '0.0', '', '0.0', '', '0.0', '', '0.0', '', '0.0']
        for j in range(len(result[i])):
            similar_word_data[j] = str(result[i][j])
        bulk_insert_arr.append(similar_word_data)
        bulk_insert_count += 1
        if bulk_insert_count >= BULK_INSERT_UNIT:
            cursor = connection.cursor()
            cursor.execute("SET NAMES utf8mb4")
            cursor.executemany(sql, bulk_insert_arr)
            connection.commit()
            bulk_insert_count = 0
            bulk_insert_arr = []

    if bulk_insert_count > 0:
        cursor = connection.cursor()
        cursor.execute("SET NAMES utf8mb4")
        cursor.executemany(sql, bulk_insert_arr)
        connection.commit()

def filter_by_category(connection, page_title, arr_most_similar):
    result = []
    page_title = page_title.replace(' ', '_')
    cursor = connection.cursor()
    cursor.execute("SET NAMES utf8mb4")
    sql = 'select page_id from page where page_title = %s and page_namespace = 0';
    data = (page_title, )
    cursor.execute(sql, data)
    if cursor.rowcount == 0:
        return []
    page_id = cursor.fetchone()[0]

    matched = 0
    for i in range(len(arr_most_similar)):
        page_title2 = arr_most_similar[i][0].replace(' ', '_')
        sql = "select "
        sql = 'select page_id from page where page_title = %s and page_namespace = 0';
        data = (page_title2, )
        cursor.execute(sql, data)
        if cursor.rowcount == 0:
            return []
        page_id2 = cursor.fetchone()[0]
        sql = "select cl_to from categorylinks where cl_from = %s and cl_to in (select cl_to from categorylinks where cl_from = %s)"
        data = (str(page_id2), str(page_id))
        cursor.execute(sql, data)
        if cursor.rowcount == 0:
            continue
        result.append(arr_most_similar[i])
        matched += 1
        if matched >= 10:
            return result
    return result

# データベースへの接続とカーソルの生成
connection = MySQLdb.connect(
    host='localhost',
    user='mysql',
    passwd='jjpC3_)#y(8g',
    db='jawiki_latest',
# テーブル内部で日本語を扱うために追加
    charset='utf8mb4'
)
cursor = connection.cursor()
cursor.execute("SET NAMES utf8mb4")

# Wikipedia語を全て取得
query = "select distinct page_title from wikipedia_textinfo"
cursor.execute(query)

arr_page_title = []
dict_page_content = {}
g = 0
for row in cursor:
    s = row[0].decode('UTF-8')
    s = s.replace('_', ' ')
    arr_page_title.append(s)
    dict_page_content[s] = ''
    g += 1
    if g % 1000 == 0:
        print('g = ' + str(g))

# Wikipedia語の説明文を全て取得し、dict_page_contentに格納
query = "select page_title, content from wikipedia_textinfo page_title order by id"
cursor.execute(query)
h = 0
for row in cursor:
    dict_page_content[row[0].decode('UTF-8')] += row[1]
    h += 1
    if h % 1000 == 0:
        print('h = ' + str(h))

# Mecabで、全ての説明文を分かち書きし、順にtraining_docsに格納
training_docs = []
token = []
i = 0
page_title = []
for k, v in dict_page_content.items():
    if len(v) > 0:
        token.append(tokenizer(v))
        page_title.append(k)
        training_docs.append(TaggedDocument(words=token[i], tags=[page_title[i]]))
        i += 1
        if i % 1000 == 0:
            print('i = ' + str(i))

# 以下、training_docsのデータを学習し、各Wikipedia語と似たWikipedia語のベスト10を表示する
model = Doc2Vec(documents=training_docs, min_count=1)

# 以下、カテゴリフィルタ無しで、類似単語を求める
result = []
result_category = []
for j in range(i):
    arr_most_similar = model.dv.most_similar(page_title[j], topn=20)
    # 以下、カテゴリフィルタ無し用の処理
    csv_content = [page_title[j], '', 0.0, '', 0.0, '', 0.0, '', 0.0, '', 0.0, '', 0.0, '', 0.0, '', 0.0, '', 0.0, '', 0.0]
    for q in range(10):
        csv_content[q * 2 + 1] = arr_most_similar[q][0]
        csv_content[q * 2 + 2] = arr_most_similar[q][1]
    result.append(csv_content)
    # 以下、カテゴリフィルタ有り用の処理
    arr_most_similar_filtered = filter_by_category(connection, page_title[j], arr_most_similar)
    csv_content_category = [page_title[j], '', 0.0, '', 0.0, '', 0.0, '', 0.0, '', 0.0, '', 0.0, '', 0.0, '', 0.0, '', 0.0, '', 0.0]
    for qq in range(min([10, len(arr_most_similar_filtered)])):
        csv_content_category[qq * 2 + 1] = arr_most_similar_filtered[qq][0]
        csv_content_category[qq * 2 + 2] = arr_most_similar_filtered[qq][1]
    result_category.append(csv_content_category)
    # 以下、処理が継続中であることを確認するための表示
    if j % 1000 == 0:
        print('j = ' + str(j))

# 以下、結果出力
output_csv(result, False)
output_database(connection, result, False)
output_csv(result_category, True)
output_database(connection, result_category, True)

# 接続を閉じる
connection.close()

10. 目視での結果評価

9章までの一連の作業で出力された結果を、様々な分野の単語をピックアップして個人的な評価をしてみたいと思います。
ここでは、「足利義政」「アセトアルデヒド」「高市早苗」「篠田麻里子」「住宅金融支援機構」「池田修一」をピックアップしました。
以下、結果です。

mysql> select * from wikipedia_similar_word where word = '足利義政'\G
*************************** 1. row ***************************
                 id: 1192078
               word: 足利義政
      similar_word1: 足利義教
 cosine_similarity1: 0.817339301109314
      similar_word2: 霊元天皇
 cosine_similarity2: 0.8108541369438171
      similar_word3: 応仁の乱
 cosine_similarity3: 0.8105510473251343
      similar_word4: 足利義輝
 cosine_similarity4: 0.806555986404419
      similar_word5: 足利義視
 cosine_similarity5: 0.8023910522460938
      similar_word6: 足利義持
 cosine_similarity6: 0.7954255938529968
      similar_word7: 明応の政変
 cosine_similarity7: 0.7828155755996704
      similar_word8: 細川勝元
 cosine_similarity8: 0.7794800400733948
      similar_word9: 赤松政則
 cosine_similarity9: 0.7720962166786194
     similar_word10: 北条時頼
cosine_similarity10: 0.7675746083259583
1 row in set (0.00 sec)

mysql> select * from wikipedia_similar_word_category where word = '足利義政'\G
*************************** 1. row ***************************
                 id: 1192078
               word: 足利義政
      similar_word1: 足利義教
 cosine_similarity1: 0.817339301109314
      similar_word2: 霊元天皇
 cosine_similarity2: 0.8108541369438171
      similar_word3: 応仁の乱
 cosine_similarity3: 0.8105510473251343
      similar_word4: 足利義輝
 cosine_similarity4: 0.806555986404419
      similar_word5: 足利義視
 cosine_similarity5: 0.8023910522460938
      similar_word6: 足利義持
 cosine_similarity6: 0.7954255938529968
      similar_word7: 明応の政変
 cosine_similarity7: 0.7828155755996704
      similar_word8: 細川勝元
 cosine_similarity8: 0.7794800400733948
      similar_word9: 北条時頼
 cosine_similarity9: 0.7675746083259583
     similar_word10: 東山天皇
cosine_similarity10: 0.7648724317550659
1 row in set (0.00 sec)

mysql> select * from wikipedia_similar_word where word = 'アセトアルデヒド'\G
*************************** 1. row ***************************
                 id: 163090
               word: アセトアルデヒド
      similar_word1: アクロレイン
 cosine_similarity1: 0.822568953037262
      similar_word2: ジアセチル
 cosine_similarity2: 0.8070136308670044
      similar_word3: 臭素中毒
 cosine_similarity3: 0.80400550365448
      similar_word4: フーゼル油
 cosine_similarity4: 0.7938266396522522
      similar_word5: トルエン
 cosine_similarity5: 0.7928942441940308
      similar_word6: エクゴニン
 cosine_similarity6: 0.789889395236969
      similar_word7: シアナミド
 cosine_similarity7: 0.7779234051704407
      similar_word8: システイン
 cosine_similarity8: 0.7777070999145508
      similar_word9: アルドリン
 cosine_similarity9: 0.7754071950912476
     similar_word10: アジ化ナトリウム
cosine_similarity10: 0.7740764021873474
1 row in set (0.01 sec)

mysql> select * from wikipedia_similar_word_category where word = 'アセトアルデヒド'\G
*************************** 1. row ***************************
                 id: 163090
               word: アセトアルデヒド
      similar_word1: アクロレイン
 cosine_similarity1: 0.822568953037262
      similar_word2: ジアセチル
 cosine_similarity2: 0.8070136308670044
      similar_word3: フーゼル油
 cosine_similarity3: 0.7938266396522522
      similar_word4: トルエン
 cosine_similarity4: 0.7928942441940308
      similar_word5: エクゴニン
 cosine_similarity5: 0.789889395236969
      similar_word6: シアナミド
 cosine_similarity6: 0.7779234051704407
      similar_word7: システイン
 cosine_similarity7: 0.7777070999145508
      similar_word8: アルドリン
 cosine_similarity8: 0.7754071950912476
      similar_word9: リノール酸
 cosine_similarity9: 0.7739605903625488
     similar_word10: ホメピゾール
cosine_similarity10: 0.7703306674957275
1 row in set (0.00 sec)

mysql> select * from wikipedia_similar_word where word = '高市早苗'\G
*************************** 1. row ***************************
                 id: 1258518
               word: 高市早苗
      similar_word1: 麻生太郎
 cosine_similarity1: 0.792701244354248
      similar_word2: 蓮舫
 cosine_similarity2: 0.7915886640548706
      similar_word3: 石破茂
 cosine_similarity3: 0.7912198305130005
      similar_word4: 枝野幸男
 cosine_similarity4: 0.7899182438850403
      similar_word5: 古賀茂明
 cosine_similarity5: 0.7750837206840515
      similar_word6: 後藤田正純
 cosine_similarity6: 0.7746227979660034
      similar_word7: 三浦瑠麗
 cosine_similarity7: 0.7708612084388733
      similar_word8: 鳩山由紀夫
 cosine_similarity8: 0.7705498337745667
      similar_word9: 橋本岳
 cosine_similarity9: 0.7663498520851135
     similar_word10: 福山哲郎
cosine_similarity10: 0.7659152150154114
1 row in set (0.00 sec)

mysql> select * from wikipedia_similar_word_category where word = '高市早苗'\G
*************************** 1. row ***************************
                 id: 1258518
               word: 高市早苗
      similar_word1: 麻生太郎
 cosine_similarity1: 0.792701244354248
      similar_word2: 蓮舫
 cosine_similarity2: 0.7915886640548706
      similar_word3: 石破茂
 cosine_similarity3: 0.7912198305130005
      similar_word4: 枝野幸男
 cosine_similarity4: 0.7899182438850403
      similar_word5: 古賀茂明
 cosine_similarity5: 0.7750837206840515
      similar_word6: 後藤田正純
 cosine_similarity6: 0.7746227979660034
      similar_word7: 三浦瑠麗
 cosine_similarity7: 0.7708612084388733
      similar_word8: 鳩山由紀夫
 cosine_similarity8: 0.7705498337745667
      similar_word9: 橋本岳
 cosine_similarity9: 0.7663498520851135
     similar_word10: 福山哲郎
cosine_similarity10: 0.7659152150154114
1 row in set (0.00 sec)

mysql> select * from wikipedia_similar_word where word = '篠田麻里子'\G
*************************** 1. row ***************************
                 id: 1121449
               word: 篠田麻里子
      similar_word1: 小嶋陽菜
 cosine_similarity1: 0.7518864274024963
      similar_word2: 蛯原友里
 cosine_similarity2: 0.7350424528121948
      similar_word3: 川口春奈
 cosine_similarity3: 0.7319941520690918
      similar_word4: 河西智美
 cosine_similarity4: 0.728840172290802
      similar_word5: 古畑星夏
 cosine_similarity5: 0.72809898853302
      similar_word6: 前田亜美
 cosine_similarity6: 0.7251523733139038
      similar_word7: 仲川遥香
 cosine_similarity7: 0.723530650138855
      similar_word8: 天羽希純
 cosine_similarity8: 0.7231149673461914
      similar_word9: 中田ちさと
 cosine_similarity9: 0.7179308533668518
     similar_word10: 弓木奈於
cosine_similarity10: 0.7172588109970093
1 row in set (0.00 sec)

mysql> select * from wikipedia_similar_word_category where word = '篠田麻里子'\G
*************************** 1. row ***************************
                 id: 1121449
               word: 篠田麻里子
      similar_word1: 小嶋陽菜
 cosine_similarity1: 0.7518864274024963
      similar_word2: 蛯原友里
 cosine_similarity2: 0.7350424528121948
      similar_word3: 川口春奈
 cosine_similarity3: 0.7319941520690918
      similar_word4: 河西智美
 cosine_similarity4: 0.728840172290802
      similar_word5: 古畑星夏
 cosine_similarity5: 0.72809898853302
      similar_word6: 前田亜美
 cosine_similarity6: 0.7251523733139038
      similar_word7: 仲川遥香
 cosine_similarity7: 0.723530650138855
      similar_word8: 天羽希純
 cosine_similarity8: 0.7231149673461914
      similar_word9: 中田ちさと
 cosine_similarity9: 0.7179308533668518
     similar_word10: 弓木奈於
cosine_similarity10: 0.7172588109970093
1 row in set (0.00 sec)

mysql> select * from wikipedia_similar_word where word = '住宅金融支援機構'\G
*************************** 1. row ***************************
                 id: 671066
               word: 住宅金融支援機構
      similar_word1: 日本ローン債権市場協会
 cosine_similarity1: 0.8466073274612427
      similar_word2: モルガン・スタンレーMUFG証券
 cosine_similarity2: 0.8440862894058228
      similar_word3: 防災業務計画
 cosine_similarity3: 0.836384654045105
      similar_word4: 勤労者退職金共済機構
 cosine_similarity4: 0.8330814242362976
      similar_word5: 小林啓孝
 cosine_similarity5: 0.830699622631073
      similar_word6: 公共法人
 cosine_similarity6: 0.8302010297775269
      similar_word7: トンガ国立準備銀行
 cosine_similarity7: 0.8284288048744202
      similar_word8: 集団投資スキーム
 cosine_similarity8: 0.8265096545219421
      similar_word9: 伊藤元久
 cosine_similarity9: 0.8256919384002686
     similar_word10: しんきん証券
cosine_similarity10: 0.8254032135009766
1 row in set (0.00 sec)

mysql> select * from wikipedia_similar_word_category where word = '住宅金融支援機構'\G
*************************** 1. row ***************************
                 id: 671066
               word: 住宅金融支援機構
      similar_word1: 勤労者退職金共済機構
 cosine_similarity1: 0.8330814242362976
      similar_word2: 小林啓孝
 cosine_similarity2: 0.830699622631073
      similar_word3: トンガ国立準備銀行
 cosine_similarity3: 0.8284288048744202
      similar_word4: 欧州投資基金
 cosine_similarity4: 0.8240056037902832
      similar_word5: 自動車技術総合機構
 cosine_similarity5: 0.8232510089874268
      similar_word6: テレコムサービス協会
 cosine_similarity6: 0.8192850947380066
      similar_word7: 木村昌人
 cosine_similarity7: 0.8176124691963196
      similar_word8:
 cosine_similarity8: 0
      similar_word9:
 cosine_similarity9: 0
     similar_word10:
cosine_similarity10: 0
1 row in set (0.00 sec)

mysql>
mysql> select * from wikipedia_similar_word where word = '池田修一'\G
*************************** 1. row ***************************
                 id: 1014285
               word: 池田修一
      similar_word1: 村山幸子
 cosine_similarity1: 0.7814040780067444
      similar_word2: 木屋太二
 cosine_similarity2: 0.7778847813606262
      similar_word3: 清成哲也
 cosine_similarity3: 0.7741992473602295
      similar_word4: 宮沢吾朗
 cosine_similarity4: 0.7726258635520935
      similar_word5: 福崎睦美
 cosine_similarity5: 0.772455096244812
      similar_word6: 小野修一
 cosine_similarity6: 0.7705255150794983
      similar_word7: 大原英二
 cosine_similarity7: 0.7694786190986633
      similar_word8: 名人戦 (チャンギ)
 cosine_similarity8: 0.7691141963005066
      similar_word9: 銭宇平
 cosine_similarity9: 0.767968475818634
     similar_word10: 高島一岐代
cosine_similarity10: 0.7675204873085022
1 row in set (0.00 sec)

mysql> select * from wikipedia_similar_word_category where word = '池田修一'\G
*************************** 1. row ***************************
                 id: 1014285
               word: 池田修一
      similar_word1: 村山幸子
 cosine_similarity1: 0.7814040780067444
      similar_word2: 小野修一
 cosine_similarity2: 0.7705255150794983
      similar_word3: 大原英二
 cosine_similarity3: 0.7694786190986633
      similar_word4: 高島一岐代
 cosine_similarity4: 0.7675204873085022
      similar_word5: 板谷進
 cosine_similarity5: 0.7648707628250122
      similar_word6: 木下浩一
 cosine_similarity6: 0.7621403336524963
      similar_word7: 依田有司
 cosine_similarity7: 0.7530767321586609
      similar_word8:
 cosine_similarity8: 0
      similar_word9:
 cosine_similarity9: 0
     similar_word10:
cosine_similarity10: 0
1 row in set (0.00 sec)

※wikipedia_similar_wordがカテゴリ縛りなし、wikipedia_similar_word_categoryがカテゴリ縛りありです。

結果の第一印象として、かなり精度が高いと思いました。Doc2Vecは正直なところ数学的にブラックボックスに近いのですが、100万以上ある単語から、当方の目で見て、ここまで意味が近い単語を抽出するとは考えていませんでした。
また、カテゴリ縛りについてはこれらの少ない例を見る限りは明らかな優位性があるとは言えませんが、唯一、最後の例の「池田修一」のみ、優位性が認識できました。
カテゴリ縛りのメリットとしては、まったく関連性のない単語が選ばれることを防ぐことができます。デメリットとしては、上記「住宅金融支援機構」「池田修一」の結果にみられるように、結果の数が10に満たない可能性があります。

2022年1月25日時点の結果は、下記のURLにアップロードしました。
https://fill-in-the-blank-question.com/jawiki_similarity/20220125/

以下、簡単に各ファイルの説明を記します。

wikipedia_similar_word.sql → カテゴリ縛りなしで類似性を解析した結果(SQL形式)
wikipedia_similar_word_category.sql → カテゴリ縛りありで類似性を解析した結果(SQL形式)
wikipedia_similarity_category_sjis.csv → カテゴリ縛りありで類似性を解析した結果(文字コードShift-JISのCSV形式)
wikipedia_similarity_category_utf8.csv → カテゴリ縛りありで類似性を解析した結果(文字コードUTF-8のCSV形式)
wikipedia_similarity_sjis.csv → カテゴリ縛りなしで類似性を解析した結果(文字コードShift-JISのCSV形式)
wikipedia_similarity_utf8.csv → カテゴリ縛りなしで類似性を解析した結果(文字コードUTF-8のCSV形式)

※Shift-JISのCSVに関しては行数が多いため、直接Excelで開くことができません。
※UTF-8のCSVに関しては、文字コードの関係で、直接Excelで開くことができません。

11. 後処理

 一連の作業で使用してきたデータベースや作業用ディレクトリは、今後の再利用が前提のため、適切にクリアしなければなりません。
そのために、以下のようなスクリプトを実行します。

# clean the database for the next update
mysql -umysql jawiki_latest -e "drop table categorylinks; drop table page; truncate wikipedia_similar_word; truncate wikipedia_similar_word_category; truncate wikipedia_textinfo;"

# clean the work space for the next update
rm -f ~/workspace/jawiki_latest/*.txt; rm -f ~/workspace/jawiki_latest/*.csv; rm -f ~/workspace/jawiki_latest/*.sql; rm -f ~/workspace/jawiki_latest/*.bz2

12. スケジュール化

 第2章の「本稿の目的」で、「投稿前に発生する作業で得られた解析結果を、個人的な業務に生かす」と述べました。また、Wikipediaのデータ自体が定期更新されるため、解析結果も定期更新しなければなりません。そのため、4章から11章の一連の作業は、一度限りの作業を除き、下記の通りスケジュール化します。

update_similarity.sh
#/usr/bin/bash

# move to the workspace
cd ~/workspace/jawiki_latest/

# get the latest wikipedia dump
wget https://dumps.wikimedia.org/jawiki/latest/jawiki-latest-pages-articles.xml.bz2 --no-check-certificate
wget https://dumps.wikimedia.org/jawiki/latest/jawiki-latest-page.sql.gz --no-check-certificate
wget https://dumps.wikimedia.org/jawiki/latest/jawiki-latest-categorylinks.sql.gz --no-check-certificate

# transfer the pages-articles data into txt files
/usr/local/rbenv/bin/rbenv exec wp2txt --input-file jawiki-latest-pages-articles.xml.bz2

# unzip the sql archives
gunzip jawiki-latest-page.sql.gz
gunzip jawiki-latest-categorylinks.sql.gz

# import the sql files to the database
mysql -umysql jawiki_latest < jawiki-latest-page.sql
mysql -umysql jawiki_latest < jawiki-latest-categorylinks.sql

# make a wikipedia dictionary for mecab
mysql -u mysql jawiki_latest -e "SELECT p.page_title page_title FROM page p where page_title <> '\"\'' GROUP BY p.page_title" | perl -pe 's;,;、;g' | perl -pe 's;\t;,;g' > /tmp/jawiki_page.csv
/opt/rh/rh-python38/root/usr/bin/python3 parse.py
/usr/libexec/mecab/mecab-dict-index -d /usr/lib64/mecab/dic/ipadic -u /tmp/user_dic.dic -f utf-8 -t utf-8 /tmp/wikiword.csv

# register wikipedia textinfo to the database
/opt/rh/rh-python38/root/usr/bin/python3 register_textinfo.py

# make csv files and sql files for similarity among wikipedia words
/opt/rh/rh-python38/root/usr/bin/python3 doc2vec_all.py

# clean the database for the next update
mysql -umysql jawiki_latest -e "drop table categorylinks; drop table page; truncate wikipedia_similar_word; truncate wikipedia_similar_word_category; truncate wikipedia_textinfo;"

# clean the work space for the next update
rm -f ~/workspace/jawiki_latest/*.txt; rm -f ~/workspace/jawiki_latest/*.csv; rm -f ~/workspace/jawiki_latest/*.sql; rm -f ~/workspace/jawiki_latest/*.bz2

毎月5日の0時0分に上記のスクリプトを実行

# crontab -l
0 0 5 * * /root/workspace/jawiki_latest/update_similarity.sh

この解析作業自体、ビッグデータを処理するようなインフラ下で実施したものではなく、またdoc2vec_all.py実行時にスワップ領域を半分以上浸食する等、利用したPCがデータ量に対してかなりのアンダースペックと考えられるため、処理速度を論じることは適切ではありませんが、上記のスクリプトを全て実行するのに、2022年1月25日の時点で、約33.5時間を要しました。

定期的な解析結果は、以下のURLに公開します。
https://fill-in-the-blank-question.com/jawiki_similarity/
各ファイルの説明は、10章で述べた通りです。

13. 参考サイト

MeCabへWikipediaの辞書を追加する方法
https://techblog.gmo-ap.jp/2020/11/10/mecab_wikipedia_dictionary/

PythonからMeCabを使う
https://qiita.com/MamoruItoi/items/b633a529d4bf5855f660

Pythonのエラー 'shift_jis' codec can't encode character '\u6c2e' の解決方法
https://qiita.com/mimuro_syunya/items/67804088aa1002bc556f

Qiita記事作成方法 初心者の備忘録
https://qiita.com/U-MA/items/996ae933ae94c5711883

14. まとめ

 月並みですが、今回実施した一連の解析作業は、解析結果の優劣はともかく、少なくとも以下の点でとても有意義でした。

・Pythonのコーディングにある程度慣れたこと
・ほんの一部ですが、機械学習の一分野である自然言語処理の手法を体験できたこと
・忘れかけていた知識を動員して、問題解決にあたることができたこと
・会社で運営している https://realint.com/quiz/ (多肢選択式問題生成工場)の定期データ更新作業を簡略化する目途がついたこと

Aidemyさんに課題を設定していただかなければ、このような有意義な作業を実施する発想は浮かばなかったでしょう。
それにとどまらず、複数回にわたり、相談に乗っていただき、Aidemyさんのスタッフの方々には大変感謝しています。

今後の展開としては、英語版のWikipediaで同様の解析をやってみたいです。
また、より大規模なコーパスを使って、Word2Vecにも挑戦してみたいです。

この記事は未だ突っ込みどころ満載だと思いますので、お気づきの点等ご指摘いただければ幸いです。
最後までお読みいただきまして、ありがとうございました。

2
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
2
1