Home About Contact
Machine Learning , Python , Groovy

Word2vec, Wikipedia の日本語コンテンツを入手してトレーニング(備忘録)

Word2vec ... いまさら感がありますが、Wikipediaダウンロードから手順を書き残します。

使用した環境は Ubuntu 22.04 LTS です。

コンテンツのダウンロード

https://dumps.wikimedia.org/jawiki/latest/にアクセスして必要なファイルをきめます。 ここでは、 jawiki-latest-pages-articles.xml.bz2 を使います。

$ curl https://dumps.wikimedia.org/jawiki/latest/jawiki-latest-pages-articles.xml.bz2 -o jawiki-latest-pages-articles.xml.bz2

内容を取り出す

必要なら pip コマンドをインストールして、 wikiextractor を入れる。

$ sudo apt install python3-pip
$ pip install wikiextractor

wikiextractor を実行:

$ python3 -m wikiextractor.WikiExtractor jawiki-latest-pages-articles.xml.bz2

これで実行したディレクトリに ./text フォルダおよび AA..BJ までの(今回の場合)サブフォルダが作成され、 その中に、wiki_00, wiki_01, ... といったファイル名でテキストファイルが作成されます。 これらのテキストファイルには docタグで区切られてwikipedia の内容が書き出されています。

例:

<doc id="4316626" url="https://ja.wikipedia.org/wiki?curid=4316626" title="細貝正統">
細貝正統

細貝 正統(ほそかい まさのり、1975年5月2日 - )は、日本の実業家。
人物.
東京都出身。学習院大学経済学部卒業。
</doc>

wiki_xx を「単語 + 半角スペース区切り」の巨大なテキストファイルひとつにまとめる

これを一回で行うこともできますが、数が多いので(3580ファイルありました)、 wiki_xx ファイルごとに処理して、結果を一旦 tmp_xx.txt ファイルに保存します。

使用した言語は groovy です。 単語ごとに分割する(わかちがき)処理は kuromoji を使いました。 名詞と動詞をターゲットにして、動詞は原型(baseForm)を使用しています。

conv.groovy

@Grab(group='org.apache.lucene', module='lucene-analyzers-kuromoji', version='8.11.2')

import org.apache.lucene.analysis.ja.JapaneseTokenizer
import org.apache.lucene.analysis.tokenattributes.CharTermAttribute
import org.apache.lucene.analysis.ja.tokenattributes.BaseFormAttribute
import org.apache.lucene.analysis.ja.tokenattributes.PartOfSpeechAttribute

class Pair<T1,T2> {
    T1 first
    T2 second
    Pair(T1 first, T2 second){
        this.first = first
        this.second = second
    }
}

def proc = { text->
    boolean underDoc = false
    def list = []
    def docList = []
    
    text.readLines().each {
        def m0 = (it =~ /^<doc/)
        def m1 = (it =~ /^<\/doc/)
    
        boolean docStartLine = m0.find()
        if( docStartLine ){
            underDoc = true
        }
        else if( m1.find() ){
            underDoc = false
            if( docList.size()>1 ){
                def title = docList[0].trim()
                def firstLine = docList[1].trim()
                boolean haveToSkipTitle = firstLine.startsWith(title)
    
                if( haveToSkipTitle ){
                    list = list + docList.drop(1)
                }
                else {
                    list = list + docList
                }
            }
            docList.clear()
        }
    
        if( underDoc && !docStartLine){
            if( it.trim()!="" ){
                docList << it
            }
        }
    }

    return list.join(System.getProperty('line.separator'))
}


def list = []

def targetDir = new File("text")
targetDir.eachDirRecurse { dir->
    dir.listFiles( { it.isFile() && it.name.startsWith('wiki_') } as FileFilter ).each {
        def m = (it.name =~ /^wiki_(.*)$/)
        if( m.find() ){
            def myname = "tmp_${m.group(1)}.txt"
            def exportFile = new File(it.parentFile, myname)

            list << new Pair(it, exportFile)
        }
        else {
            assert false
        }
    }
}

def total = list.size()

list.eachWithIndex { pair, index->
    def wikiFile = pair.first
    def exportFile = pair.second

    if( index%100==0 ){
        println("- ${index}/${total} (${(index*1f)/(total*1f)*100f}%)")
    }

    if( !exportFile.exists() ){
        def text = proc(wikiFile.text)
    
        def wordList = []
    
        def jt = new JapaneseTokenizer(null, false, JapaneseTokenizer.Mode.NORMAL)
        jt.setReader(new StringReader(text))
        jt.reset()
        while(jt.incrementToken()){
            def ct  = jt.addAttribute(CharTermAttribute.class)
            def bfa = jt.addAttribute(BaseFormAttribute.class)
            def posa= jt.addAttribute(PartOfSpeechAttribute.class)
    
            if( posa.partOfSpeech.startsWith('名詞') ){
                wordList << ct.toString()
            }
            else if( posa.partOfSpeech.startsWith('動詞') ){
                if( bfa.baseForm!=null ){
                    wordList << bfa.baseForm
                } else {
                    wordList << ct.toString()
                }
            }
        }
        jt.close()
    
        if( wordList.size()>0 ){
            exportFile.text = wordList.join(' ')
        }
    }
}

実行します。

$ groovy conv

次に生成した tmp_xx.txt をひとつのテキストファイルにまとめます。

merge.groovy

class Pair<T1,T2> {
    T1 first
    T2 second
    Pair(T1 first, T2 second){
        this.first = first
        this.second = second
    }
}

def list = []

def rootDir = new File("text")
rootDir.eachDirRecurse { dir->
    dir.listFiles( { it.isFile() && it.name.startsWith('wiki_') } as FileFilter ).each {
        def m = (it.name =~ /^wiki_(.*)$/)
        if( m.find() ){
            def myname = "tmp_${m.group(1)}.txt"
            def exportFile = new File(it.parentFile, myname)
            list << new Pair(it, exportFile)
        }
        else {
            assert false
        }
    }
}

def resultFile = new File("jawiki_words.txt")
def pw = new PrintWriter(new OutputStreamWriter(new FileOutputStream(resultFile)))

list.eachWithIndex { pair, index->
    def exportFile = pair.second
    assert exportFile.exists()

    //assert exportFile.text.readLines().size()==1
    pw.print( exportFile.text.readLines()[0] )
    if( index<(list.size()-1) ){
        // 最後以外は 半角スペースで区切ってつなげていく.
        pw.print(' ')
    }
}

pw.flush()
pw.close()

実行します。

$ groovy merge

結果は jawiki_words.txt にできます。これは、以下のような内容になっています。(一部抜粋)

タイ 勝負事 勝敗 決定 する 結果 チュニジア 共和 国 チュニジア きょう 通称 チュニジア 北 アフリカ マグリブ 位置 する 共和 制 国家 西 アルジェリア 南東 リビア 国境 接す 北 東 地中海 面する 地中海 対岸 北東 東 イタリア 領土 パンテッレリーア 島 ランペドゥーザ 島 シチリア 島 ある 地中海 島国 マルタ 首都 チュニス 概要 . アフリカ 世界 地中海 世界 アラブ 世界 一員 アフリカ 連合 アラブ 連盟 地中海 連合 アラブ マグレブ 連合 加盟 する いる 同国 歴史 上 アフリカ 呼ぶ れる アフリカ 大陸 名前 由来 なる 地域 国名

gensim を使って Word2vec する

gensim をインストール:

$ pip install gensim

buildmodel.py

import logging
import os

from gensim.models import word2vec

logging.basicConfig(format='%(asctime)s : %(levelname)s : %(message)s', level=logging.INFO)

data_dir = os.path.join(os.path.dirname(__file__), "data")
sentences = word2vec.Text8Corpus(os.path.join(os.path.dirname(__file__), "jawiki_words.txt"), 50)

model = word2vec.Word2Vec(sentences, vector_size=300, min_count=30)

model.init_sims(replace=True)
model.save("word2vec_gensim.bin")

先ほど作成した jawiki_words.txt を指定して 50 単語ごとをひとつの文とみなして word2vec します。 結果の model は word2vec_gensim.bin ファイルに保存。

手元のマシンでは、2時間程度でモデルができました。

できあがったモデルを試します。

from gensim.models import word2vec
model = word2vec.Word2Vec.load("word2vec_gensim.bin")

print( model.wv.most_similar("昭和") )
print( model.wv.most_similar("ウクライナ") )

実行:

$ python3 test.py

[('明治', 0.6704527139663696),
 ('大正', 0.5379592180252075),
 ('平成', 0.5278304219245911),
 ('1961', 0.5251448154449463),
 ('1957', 0.5250473022460938),
 ('1963', 0.521145224571228),
 ('1958', 0.5202600955963135),
 ('1959', 0.5163960456848145),
 ('1965', 0.5132789015769958),
 ('1972', 0.5079615116119385)]

[('ロシア', 0.8216716051101685),
 ('ベラルーシ', 0.7999508380889893),
 ('ポーランド', 0.7456522583961487),
 ('アゼルバイジャン', 0.7255234122276306),
 ('ルーマニア', 0.7122900485992432),
 ('モルドバ', 0.7117916345596313),
 ('エストニア', 0.7099877595901489),
 (' リトアニア', 0.6905235052108765),
 ('アルメニア', 0.6896757483482361),
 ('グルジア', 0.6720161437988281)]

まあこんなもんでしょうか。