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>
これを一回で行うこともできますが、数が多いので(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(' ')
}
}
}
- proc 関数で docタグを取り除いた上で、タイトルの除去を行っています。
- 途中で中断して、再実行できます。(すでに処理した wiki_xx は処理をスキップ)
実行します。
$ 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 をインストール:
$ 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)]
まあこんなもんでしょうか。