Apitore blog

Apitoreを運営していた元起業家のブログ

deeplearning4jで日本語WikipediaのWord2Vecを作る

はじめに

Web APIのマーケットプレイスであるApitoreに、Word2Vecを追加しようと思います。Word2Vecがあれば自然言語処理系のアプリケーションで色々な拡がりが出てきます。その話はAPIを公開したときにするとして、今回はJavaでWord2Vecを実装するノウハウを公開します。JavaでWord2Vecを作るなら、本家のGoogleでもオススメしているdeeplearning4jを使うと簡単です。内蔵している形態素解析機能はスペース区切りなので、日本語形態素解析器のKuromojiを使います。 amarec (20160919-095417)

WikipediaのDumpの取得

2016/9/15時点のDump(jawiki-latest-pages-articles.xml.bz2)を用いました。WikipediaのDumpは、データの日時を明記していれば自由に使って良いそうです。学習データに使う場合は特に日時の明記は必要ない気がしますが、一応明記しておきます。Dumpはこちらから取得できます。約2.2GBあります。圧縮ファイルになっていますが、後述するやり方で展開するので、解凍しなくてOKです。

WikipediaのDumpの変換

XMLになっているので、TXTに変換します。偉大なる先人がたくさんいまして、今回はRubyのwp2txtを使わせてもらいます。 私はWindows10-64bitでCygwin上で実行しました。もしかしたらWindows10のBashでも出来るかもしれません。cygwinにはgccとrubyを予め入れておきましたが、wp2txtで使うnokogiriのインストールに失敗するので、追加で「ruby-devel」「libxml2-devel」「libxslt-devel」をCygwinでインストールしました。wp2txtのインストールは以下のコマンドで完了します。

$ gem install wp2txt

インストールが成功したら、wp2txtを使ってWikipediaのDumpをTxtファイルにします。以下のコマンドを実行します。実行に2時間くらいかかりました。全部で531ファイル、約5.2GBくらいあります。

$ wp2txt --input-file jawiki-latest-pages-articles.xml.bz2

wp2txtでTXTファイルを出力すると一行置きに空行が挿入されるので、データを一つにまとめるついでにそれも除去します。

$ cat jawiki-latest-pages-articles.xml-* | grep -v '^\s*$' > jawiki-corpus.txt

これでデータの準備はOKです。 次はプログラム部分を解説します。とりあえず試す分には全データを使う必要はないので、10,000行くらい抜き出しておくと良いでしょう。

deeplearning4jによるword2vec

Javaをどうしても使いたいので、deeplearning4jを利用します。形態素解析器はKuromojiです。今回はMavenを使います。

<dependency>
  <groupId>com.atilika.kuromoji</groupId>
  <artifactId>kuromoji-ipadic</artifactId>
  <version>0.9.0</version>
</dependency>
<dependency>
  <groupId>org.deeplearning4j</groupId>
  <artifactId>deeplearning4j-ui</artifactId>
  <version>0.5.0</version>
</dependency>
<dependency>
  <groupId>org.deeplearning4j</groupId>
  <artifactId>deeplearning4j-nlp</artifactId>
  <version>0.5.0</version>
</dependency>
<dependency>
  <groupId>org.nd4j</groupId>
  <artifactId>nd4j-native</artifactId>
  <version>0.5.0</version>
</dependency>

deeplearning4jのword2vecでkuromojiを利用するために、拡張クラスを実装します。TokenizerとTokenizerFactoryの拡張クラスです。既にScalaで同様のことをやられた方がいたので、そちらを参考にしました。私の実装もGithubに上げておきます。

public class KuromojiIpadicTokenizer implements Tokenizer {
  private List<Token> tokens;
  private int index;
  private TokenPreProcess preProcess;
  public KuromojiIpadicTokenizer (String toTokenize) {
    com.atilika.kuromoji.ipadic.Tokenizer tokenizer = new com.atilika.kuromoji.ipadic.Tokenizer();
    tokens = tokenizer.tokenize(toTokenize);
    index = (tokens.isEmpty()) ? -1:0;
  }
  @Override
  public int countTokens() {
    return tokens.size();
  }
  @Override
  public List<String> getTokens() {
    List<String> ret = new ArrayList<String>();
    while (hasMoreTokens()) {
      ret.add(nextToken());
    }
    return ret;
  }
  @Override
  public boolean hasMoreTokens() {
    if (index < 0)
      return false;
    else
      return index < tokens.size();
  }
  @Override
  public String nextToken() {
    if (index < 0)
      return null;
    Token tok = tokens.get(index);
    index++;
    if (preProcess != null)
      return preProcess.preProcess(tok.getSurface());
    else
      return tok.getSurface();
  }
  @Override
  public void setTokenPreProcessor(TokenPreProcess preProcess) {
    this.preProcess = preProcess;
  }
}
public class KuromojiIpadicTokenizerFactory implements TokenizerFactory {
  private TokenPreProcess preProcess;
  @Override
  public Tokenizer create(String toTokenize) {
    if (toTokenize == null || toTokenize.isEmpty()) {
      throw new IllegalArgumentException("Unable to proceed; no sentence to tokenize");
    }
    KuromojiIpadicTokenizer ret = new KuromojiIpadicTokenizer(toTokenize);
    ret.setTokenPreProcessor(preProcess);
    return ret;
  }
  @Override
  public Tokenizer create(InputStream paramInputStream) {
    throw new UnsupportedOperationException();
  }
  @Override
  public void setTokenPreProcessor(TokenPreProcess preProcess) {
    this.preProcess = preProcess;
  }
  @Override
  public TokenPreProcess getTokenPreProcessor() {
    return this.preProcess;
  }
}

さて、実際に学習してみます。学習データの読み込みは以下のようにします。

SentenceIterator iter = new BasicLineIterator(new File("corpus.txt"));

形態素解析の実行は以下のようにします。形態素解析実行後に英単語の活用形部分(e.g. -ed,-ing)の除去、英字小文字化、数字の記号化をしておきます。

final EndingPreProcessor preProcessor    = new EndingPreProcessor();
KuromojiIpadicTokenizerFactory tokenizer = new KuromojiIpadicTokenizerFactory();
tokenizer.setTokenPreProcessor( new TokenPreProcess()
{
  @Override
  public String preProcess( String token )
  {
    token       = token.toLowerCase();
    String base = preProcessor.preProcess( token );
    base        = base.replaceAll( "\\d" , "__NUMBER__" );
    return base;
  }
});

学習を実行します。パラメータはネットで調べてよく使われていそうなものを採用していますので、適当です。コアは6個使うようにしましたが、タスクマネージャを見ると6個は使ってない気がします。

int batchSize   = 1000;
int iterations  = 5;
int layerSize   = 150;
Word2Vec vec = new Word2Vec.Builder()
    .batchSize(batchSize)
    .minWordFrequency(5)
    .useAdaGrad(false)
    .layerSize(layerSize)
    .iterations(iterations)
    .seed(1)
    .windowSize(5)
    .learningRate(0.025)
    .minLearningRate(1e-3)
    .negativeSample(10)
    .iterate(iter)
    .tokenizerFactory(tokenizer)
    .workers(6)
    .build();
vec.fit();

学習モデルを保存します。これを忘れると地獄です。

WordVectorSerializer.writeWordVectors(vec, "model-wordvectors.txt");

学習が完了した後は、作ったモデルを使ってアレコレできます。

WordVectors vec = WordVectorSerializer.loadTxtVectors(new File("model-wordvectors.txt"));
Collection<String> lst = vec.wordsNearest("day", 10);
System.out.println(lst);
double cosSim = vec.similarity("day", "night");
System.out.println(cosSim);
double[] wordVector = wordVectors.getWordVector("day");
System.out.println(wordVector);

おわりに

deeplearning4jを使ってWord2Vecが簡単に実装できました。肝心のデモについては準備中です。APIを公開するときにデモ結果を含めて記事にします。ちなみに小さいデータで動作確認はしているので、上記の記事は正確です。現在、Windows10 64bit corei7、メモリ10GBを使って学習中です。丸2日経っても終わっていません。メモリは10GB指定しましたが、だいたい3GB~5GBくらいしか使ってなさそうです。

Github

https://github.com/keigohtr/dl4j-word2vec-kuromoji