Python
初心者
機械学習
gensim
word2vec

Pythonによるスクレイピング&機械学習のお勉強その6-2 - Word2Vecで文章をベクトル変換しよう

今回の目標

このシリーズでは教科書(文献1)に沿ってPythonによるスクレイピングと機械学習を学びます。今回は第6章「テキスト解析とチャットボットの作成」から6-2「Word2Vecで文章をベクトル変換しよう」を学びます。

このシリーズの学習では、原則教科書のサンプルプログラムを作成してゆきますが、著作権に配慮し、できるだけそのままではなく類題を作成して勉強してゆく方針です。

教科書で勉強することの整理

この節ではWord2Vecを用いて様々な文章に出てくる単語の解析を行う手法が解説されています。Word2Vecの実装としてはGensimを用いています。教科書のサンプルプログラムでは

  • word2vec-kokoro.pyで「こころ」の文章をWord2Vecモデル化して保存
  • 対話環境で「こころ」のモデルに出てくる単語の類義語を抽出
  • 青空文庫から夏目漱石、芥川龍之介、太宰治の作品を一括ダウンロード
  • word2vec-split.pyで作品テキストを分かち書きに変換
  • word2vec-mkmodel.pyで分かち書きテキストからWord2Vecモデルを作成
  • word2vec-test.pyでいくつかの単語の類義語表示をテスト
  • 対話環境でベクトル化された単語の加算、減算で遊んでみる
  • Wikipedia日本語版の全文データのダウンロード
  • Rubyツールのwp2txtでXMLをプレーンテキストに変換
  • コマンドラインのmecabで分かち書きに変換
  • wiki-mkdic.pyでWord2Vecのモデルファイルを作成
  • 対話環境でいくつかの単語の類義語を抽出、ベクトル化された単語の加算減算で遊ぶ

と盛りだくさんの内容になっています。
今回は類題として青空文庫のデータを使ってWord2Vecの実験をしてみたいとおもいます。WikipediaはXMLからの加工が大変なことや、データが巨大なこともあるので教科書通りのコードを試してみるだけにします(本記事には非掲載)。

Word2Vecとは

Word2Vecとは、文章中の単語同士の関係性を解析してそれぞれの単語をベクトル化して表現する手法です。ベクトルの次元数はユーザーが好きに指定することができるようです。特定のソフトウェアを指すわけではなく、Word2Vecの機能を実現しているライブラリは複数存在するようです。今回教科書ではGensimというライブラリが用いられています。
ベクトル化することで機械学習のパラメータとして用いやすくなったり、線形計算ができるようになったりするというのがミソのようです。

方法と結果

  • 準備

その5-1/5-2/5-3で作成した学習用docker環境でpythonの実行を行います。前回その5-7/5-8でKerasまでインストールした状態です。

$ docker run -i -t -v $HOME:$HOME -p 8888:8888 -p 6006:6006 \
-e LC_CTYPE="C.UTF-8" -e MPLBACKEND="agg" pylearn2 /bin/bash

GPUマシンで実行するときは以下のようにします。

$ sudo docker run --runtime=nvidia -i -t -v $HOME:$HOME -p 6006:6006 -p 8888:8888 \
-e LC_CTYPE="C.UTF-8" -e MPLBACKEND="agg" pylearn2-gpu /bin/bash

今回はWord2Vecを使うためにGensimをインストールします。インストールしたら一旦dockerイメージを更新します。

# pip install gensim #(docker上)
#(インストールメッセージは略)
# exit

$ docker ps -l
CONTAINER ID        IMAGE               COMMAND             CREATED             STATUS                      PORTS               NAMES
69a9451b76f9        pylearn2            "/bin/bash"         3 hours ago         Exited (0) 13 seconds ago                       hardcore_wiles

$ docker commit 69a9451b76f pylearn2:latest
sha256:60227c248e6b016e70b5749fa179f75f0875b6548cd9936ea0ae8c9b69c62579

$ docker images | grep pylearn2
pylearn2                 latest              60227c248e6b        Less than a second ago   2.01GB

類題6-2-1 言語解析用の簡易ライブラリを作成

この節を含めて、しばらく教科書では自然言語解析の課題を扱います。この際、青空文庫やWikipediaの圧縮ファイルを読み込んだりWord2Vecのモデルを作成したりという共通作業が頻繁にでてくることになっています。そこで今回はこれらの共通作業をライブラリ化してみようと思います。
ライブラリファイルはpylearn_text.pyとします。

pylearn_text.pyに組み込む機能

  1. 青空文庫のZIPファイルから作品リストを得る機能
  2. 青空文庫のZIPファイルから作品のテキストデータ(ヘッダ、フッタ、ルビを削除)を得る機能
  3. テキストデータを単語ごとにスペースで区切った「分かち書き」に変換する機能
  4. 分かち書きテキストをWord2Vecモデルに変換し、保存する機能
  5. 形態素解析をしたテキストから、ランダムに名詞を抜き出す機能

青空文庫の作家別一括ダウンロードは教科書に従い、以下から行います。
http://keison.sakura.ne.jp/

類題6-2-1 実装

pylearn_text.pyにコードします。

pylearn_text.py
#!/usr/bin/env python
# -*- coding: utf-8 -*-

##### pylearn_text.py #####
# 学習用テキスト解析のための頻出機能ライブラリ
#
# 変更履歴
# v1.0 初版 2018/7/3
#
#
#####

import os.path
import os
import re
from zipfile import ZipFile
import random
import codecs
from janome.tokenizer import Tokenizer
from gensim.models import word2vec


# sjisの文字列をutf-8に変換するコンビニエンス関数
def sjis2utf8(sjisstr):
    cp437 = sjisstr.encode('cp437')
    s = cp437.decode('cp932')
    return s

# 特定のディレクトリ下にあるZipファイルをリストする
def listZipFiles(directory):
    #ディレクトリはNoneではならない
    if directory is None:
        raise ValueError('listZipFiles: directory cannot be None')
    if not os.path.exists(directory):
        raise ValueError('listZipFiles: ' + str(directory) + ' does not exists')
    if not os.path.isdir(directory):
        raise ValueError('listZipFiles: ' + str(directory) + ' is not a directory')

    #directory内のファイルをリストする
    wholelist = os.listdir(directory)
    ziplist = []
    for f in wholelist:
        if os.path.splitext(f)[1].lower() == '.zip': #拡張子がzipなら
            ziplist.append(os.path.join(directory, f))
    return ziplist


# 青空文庫のZipファイルの作者名を取り出す
def extractCreator(zipfile):

    with ZipFile(zipfile, mode='r') as zfp:
        namelist = zfp.namelist()
        return os.path.dirname(namelist[0])

# 青空文庫のZipファイルから作品リストを取り出す
def extractNovelList(zipfile):

    titlelist = []
    with ZipFile(zipfile, mode='r') as zfp:
        namelist = zfp.namelist()
        for name in namelist:
            title = os.path.splitext(os.path.basename(name))[0]
            titlelist.append(title)
    return titlelist

# 青空文庫のZipファイルから特定の作品のテキストを取り出す
def extractText(zipfile, noveltitle):

    creator = extractCreator(zipfile)
    text = None
    #Zipファイルからテキストを抽出
    with ZipFile(zipfile, mode='r') as zfp:
        textname = creator + '/' + noveltitle + '.txt'
        try:
            with zfp.open(textname, 'r') as fp:
                rawdata = fp.read()
                text = rawdata.decode('shift_jis', errors='ignore')
        except:
            return ''

    #ヘッダーとフッター、ルビを削除
    hdrsplit = re.split(r'\-{5,}',text)
    if len(hdrsplit) >= 3:
        text = re.split(r'\-{5,}',text)[2]

    text = re.split(r'底本:', text)[0]
    text = text.replace('|', '')
    text = re.sub(r'《.+?》', '', text)
    #脚注を削除
    text = re.sub(r'[#.+?]', '', text)

    text = text.strip()
    return text


# テキストデータを解析し、分かち書きにする
def text2wakati(textdata):
    tk = Tokenizer()

    lines = textdata.split('\r\n')
    print('分かち書き処理: 全部で', len(lines), '行の処理を行います')
    results = []
    counter = 0
    for line in lines:
        tokens = tk.tokenize(line)
        r = [] # rは1行の中身をリスト化したもの
        for token in tokens:
            if token.base_form == '*':
                w = token.surface
            else:
                w = token.base_form
            r.append(w)
        rl = (' '.join(r)).strip() #rをスペースで区切りながら結合
        results.append(rl)
        counter += 1
        if counter % 1000 == 0:
            print(counter, '行分の処理が終了しました。')
    return '\n'.join(results) # resultsを改行で区切りながら結合

# 分かち書きデータからWord2Vecのモデルを作成する
# dimensionはベクトルの次元数。デフォルトは100
def createW2VModel(wakatidata, dimension=100):
    #wakatidataを一旦ファイルに保存する
    tmpfile = 'tmp.txt'
    with open(tmpfile, mode='w') as fp:
        fp.write(wakatidata)
    data = word2vec.LineSentence(tmpfile)
    model = word2vec.Word2Vec(data, size=dimension)
    os.remove(tmpfile)
    return model

# Word2Vecモデルデータをセーブする
def saveW2VModel(modeldata, filename):
    modeldata.save(filename)
    print(filename, 'をセーブしました。')

# テキストデータから名詞だけをランダムに抜き出す
# 複数ほしいときはnumberで指定し、iterableで返す
def randomNoun(textdata, number=1):
    tk = Tokenizer()
    nounlist = []
    lines = textdata.split('\r\n')
    for line in lines:
        tokens = tk.tokenize(line)
        for token in tokens:
            hinsi = token.part_of_speech.split(',')[0]
            #print(hinsi)
            if hinsi == '名詞':
                nounlist.append(token.surface)
    print('len(nounlist)=', len(nounlist))
    print('number=', number)
    return random.sample(nounlist, number)

類題6-2-2 青空文庫の作品をベクトル化し、類義語を表示する

それでは類題として青空文庫の作者別zipファイルをダウンロードし、実験してみます。
教科書では夏目漱石、芥川龍之介、太宰治を使用していましたが、なぜか夏目漱石の作品のダウンロードができなかったので、代わりに森鴎外のものを使用します。
ダウンロードは手動で行い、pythonプログラムを置いているカレントディレクトリの下にaozoraディレクトリを作成し、そこにzipファイルを配置します。
実験は、それぞれのWord2Vecモデルを作成し、適当な名詞の類義語を表示するというものです。

類題6-2-2 実装

aozora_w2v_cclef.py
#!/usr/bin/env python
# -*- coding: utf-8 -*-

import pylearn_text as plt
import os.path
import random
from gensim.models import word2vec

##### 青空文庫のデータからWord2Vecのモデルを作成する実験 #####
AOZORADIR = './aozora/'


### ここからメインプログラム

# 乱数を初期化
random.seed()

# ディレクトリ内のZipファイルをリストする
ziplist = plt.listZipFiles(AOZORADIR)

# 作者名をリストアップする
creators = []
for zfile in ziplist:
    creators.append(plt.extractCreator(zfile))

# 作者名をsjisからutf8に変換
for i in range(len(creators)):
    creators[i] = plt.sjis2utf8(creators[i])

print('抽出された作者リスト:', creators)

models = []
fulltexts = []

# 作者ごとにWord2Vecモデルを作成し、保存する
for zipfile in ziplist:
    print(zipfile, 'からのモデル作成を行います')
    creator = plt.extractCreator(zipfile)
    creator = plt.sjis2utf8(creator) #文字コードを変換
    print('作者:', creator)
    modelfile = os.path.join(AOZORADIR, creator + '.model')

    novels = plt.extractNovelList(zipfile)
    texts = []
    # 作品ごとにテキストを取り出す
    for novel in novels:
        text = plt.extractText(zipfile, novel)
        texts.append(text)
        print(plt.sjis2utf8(novel), 'を追加しました')
    fulltext = '\n\n'.join(texts)
    fulltexts.append(fulltext)
    print(creator, 'のテキスト追加が完了しました。')
    if not os.path.exists(modelfile):
        print(creator, 'の分かち書き処理に入ります。')
        fullwakati = plt.text2wakati(fulltext)
        print(creator, 'の分かち書き処理が完了しました。')
        print(creator, 'のWord2Vecモデル作成に入ります。')
        fullmodel = plt.createW2VModel(fullwakati)
        print(creator, 'のWord2Vecモデル作成が完了しました。')

        #モデルを保存する
        plt.saveW2VModel(fullmodel, modelfile)
        models.append(fullmodel)
    else:
        models.append(word2vec.Word2Vec.load(modelfile))
        print(modelfile, 'をロードしました。')

# 名詞を適当に抜き出して、類義語を表示

# テキストの順番をシャッフル
print('作家順をシャッフルします')
random.shuffle(fulltexts)

# 名詞を抜き出す
print('名詞をランダムに抽出します')
nouns = plt.randomNoun(fulltexts[0], 3)
print('抽出された名詞:', nouns)

# 類義語を表示
print(len(models), '個のモデルがロードされています')
for model in models:
    for noun in nouns:
        print(noun, 'の類義語')
        try:
            print(model.most_similar(positive=[noun]))
        except:
            print('このモデルには', noun, 'の類義語はないようです')
    print(nouns[0], '-', nouns[1], '+', nouns[2], '=')
    try:
        print(model.most_similar(positive=[nouns[0], nouns[2]], negative=[nouns[1]]))
    except:
        print('例外が発生しました(おそらく名詞の一部がモデルに存在しません)')

類題6-2-2 実行結果

# python ./aozora_w2v_cclef.py
抽出された作者リスト: ['太宰治', '芥川龍之介', '森鴎外']
./aozora/dazai.zip からのモデル作成を行います
作者: 太宰治
「グッド・バイ」作者の言葉 を追加しました
「人間キリスト記」その他 を追加しました
「惜別」の意図 を追加しました
#(中略)
鬱屈禍 を追加しました
貪婪禍 を追加しました
太宰治 のテキスト追加が完了しました。
./aozora/太宰治.model をロードしました。
./aozora/akutagawa.zip からのモデル作成を行います
作者: 芥川龍之介
「ケルトの薄明」より を追加しました
「仮面」の人々 を追加しました
「菊池寛全集」の序 を追加しました
「鏡花全集」目録開口 を追加しました
「侏儒の言葉」の序 を追加しました
#(中略)
饒舌 を追加しました
鴉片 を追加しました
鸚鵡 ――大震覚え書の一つ―― を追加しました
芥川龍之介 のテキスト追加が完了しました。
./aozora/芥川龍之介.model をロードしました。
./aozora/mori.zip からのモデル作成を行います
作者: 森鴎外
「言語の起原」附記 を追加しました
『新訳源氏物語』初版の序 を追加しました
Resignation の説 を追加しました
あそび を追加しました
アンドレアス・タアマイエルが遺書 を追加しました
うたかたの記 を追加しました
うづしほ を追加しました
#(中略)
老人 を追加しました
鰐 を追加しました
薔薇 を追加しました
鴉 を追加しました
fig45209_01 を追加しました
興津弥五右衛門の遺書 を追加しました
森鴎外 のテキスト追加が完了しました。
./aozora/森鴎外.model をロードしました。
作家順をシャッフルします
名詞をランダムに抽出します
len(nounlist)= 438644
number= 3
抽出された名詞: ['まれ', 'もの', '仆']
3 個のモデルがロードされています
まれ の類義語
[('同じく', 0.9339747428894043), ('およそ', 0.9292280673980713), ('贈物', 0.9278705716133118), ('鷺', 0.9261468648910522), ('但し', 0.9250299334526062), ('さては', 0.9246087074279785), ('シク', 0.9238779544830322), ('序', 0.9208661317825317), ('行光', 0.920831561088562), ('蚊', 0.919930100440979)]
もの の類義語
[('わけ', 0.7898831367492676), ('言葉', 0.7680431008338928), ('事実', 0.7430917024612427), ('理由', 0.7162505984306335), ('筈', 0.7020523548126221), ('事', 0.697881817817688), ('こと', 0.6921236515045166), ('意味', 0.681252121925354), ('必要', 0.6793113350868225), ('人間', 0.6609783172607422)]
仆 の類義語
このモデルには 仆 の類義語はないようです
まれ - もの + 仆 =
例外が発生しました(おそらく名詞の一部がモデルに存在しません)
まれ の類義語
[('竿', 0.926305890083313), ('赤旗', 0.9256101846694946), ('百姓', 0.92149817943573), ('幟', 0.9214410185813904), ('被害', 0.9197931289672852), ('平治', 0.9158087372779846), ('新富', 0.9153456687927246), ('開始', 0.9124570488929749), ('四郎', 0.9108492136001587), ('内外', 0.9076755046844482)]
もの の類義語
[('こと', 0.7829136848449707), ('筈', 0.7737077474594116), ('事実', 0.7466406226158142), ('必要', 0.7441690564155579), ('人間', 0.7352352738380432), ('事', 0.7176448106765747), ('問題', 0.708543598651886), ('ところ', 0.665256917476654), ('意味', 0.6601157188415527), ('理由', 0.6536643505096436)]
仆 の類義語
[('打ち倒す', 0.91386878490448), ('ひしぐ', 0.9097530245780945), ('せく', 0.8967266082763672), ('貫く', 0.8955512642860413), ('掲載', 0.8953766226768494), ('駆る', 0.8888341188430786), ('祟る', 0.887584924697876), ('引き出す', 0.8857738971710205), ('誘う', 0.8847118020057678), ('悩ます', 0.8821281790733337)]
まれ - もの + 仆 =
[('人波', 0.8731458187103271), ('菜', 0.872932493686676), ('蘇', 0.8601065278053284), ('十文字', 0.85980623960495), ('蹌踉', 0.8575074076652527), ('烏', 0.8552427291870117), ('投げ出す', 0.8542996048927307), ('バット', 0.852385401725769), ('菅笠', 0.8521823883056641), ('コオト', 0.8520819544792175)]
まれ の類義語
[('防', 0.8475586175918579), ('すい', 0.8414316773414612), ('痛い', 0.8291813731193542), ('惱', 0.8270306587219238), ('調度', 0.8204197287559509), ('そち', 0.8175674080848694), ('駁する', 0.8128330111503601), ('さだめる', 0.8108951449394226), ('惠', 0.8097095489501953), ('娯', 0.8091545701026917)]
もの の類義語
[('事実', 0.7691612839698792), ('こと', 0.7405841946601868), ('問題', 0.6781652569770813), ('わけ', 0.6645253896713257), ('語', 0.660005509853363), ('意味', 0.6580939888954163), ('思想', 0.6549842953681946), ('人物', 0.6534765958786011), ('筈', 0.645498514175415), ('生活', 0.6347183585166931)]
仆 の類義語
このモデルには 仆 の類義語はないようです
まれ - もの + 仆 =
例外が発生しました(おそらく名詞の一部がモデルに存在しません)

何度かやってみると、類義語は結構当たっていると思えるものもあれば、「???」となるものもあるようです。また、名詞の認識が多少誤認識があるようですが、これは形態素解析ライブラリの問題なのでどうしようもないような気がします。

今回達成したこと

  • Word2Vecの基本を学びました。
  • テキスト解析用の自作簡易ライブラリを作成しました。

参考文献

  1. クジラ飛行机, Pythonによるスクレイピング&機械学習[開発テクニック], ソシム株式会社, 2016