ナード戦隊データマン

機械学習と自然言語処理についてのブログ

MTNTデータセットとは何か

MTNT1は、Redditから収集されたノイズありテキストに対する翻訳のためのテストベッドです。

ダウンロード

データセットは以下からダウンロードできます。

https://www.cs.cmu.edu/~pmichel1/mtnt/

ノイズありテキストとは

ソーシャルメディアのテキストにはいくつかの種類のノイズがあります。

  • スペルミス, タイポ: across -> accross
  • 文法ミス: a ton of -> a tons of
  • 話し言葉: want to -> wanna
  • ネットスラング: to be honest -> tbh
  • 固有名詞: Reddit -> reddit
  • 方言: 東北弁
  • 言語の入れ替え: This is so cute -> This is so kawaii
  • 専門用語: Redditの場合は、"upvote"などの単語
  • 絵文字
  • 時々マスクされるProfanities: f*ck

こういうノイズに対して、今までのMTシステムは弱いといわれています。

適応問題として捉えられるか?

ある程度まではノイズの問題は適応問題の一種として捉えられるかもしれません。しかし、特定の単語が一般ドメインとは異なる方法で翻訳されたり、入力される文法の不一致や単語エラーなどのバリエーションが増すので、適応を実行してもノイズに起因するエラーが出てきてしまいます。

MTNTはどのように収集されているか

f:id:mathgeekjp:20190821104539g:plain

3.1. reddit APIからコメントを収集。

3.3. urlや他言語やボットを排除。

3.3 トーカナイズして、小文字にして、markdownをstripする。

3.3 (optional) OOV語を含むコメントだけ残す。

3.3 subword LM scoreによるフィルタリング。(BPEによる言語モデル)

3.4 15kごとにコメントを手動翻訳のための外部ベンダーに送信し、翻訳を受け取る。受け取った翻訳は品質にばらつきがあるため、品質を検証したもの1000件をテストデータにし、残りを訓練データにする。

データソースは以下

  • 英語: 英語は圧倒的なデータ量があるため、コミュニティを限定しない。03/27/2018-03/29/3018
  • フランス語: /r/france, /r/quebec, /r/rance 09/2018-03/2018
  • 日本語: /r/newsokur, /r/bakanewsjp, /r/newsokuvip, /r/lowlevelaware, /r/steamr 11/2017-03/2018

ノイズ解析

  • Grammarlyというツールを使って英語の文法チェックをする。
  • フランス語と日本語はMicrosoft Wordの統合スペルチェッカーでテストする。
  • 正規表現を使い、絵文字や"f*ck"のような語の数をカウントする。
  • 100ワードごとのノイズカウントを記録する。

f:id:mathgeekjp:20190821105141g:plain

  • MTNTのノイズは、ニュースコーパスに比べて有意に高い。ただし、日本語は言語としてスペルミスが少ない傾向にある。JESCのほうがProfanitiesは多い。

実験モデル

  • 実験に使うモデルは、XNMTツールキットを使用してDyNetで実装。 すべての言語ペアにほぼ同じ設定を使用。
  • エンコーダーは2層の双方向LSTM、アテンションメカニズムは多層パーセプトロンデコーダーは2層LSTM。
  • 埋め込み次元は512で、他のすべての次元は1024。
  • ターゲットワードの埋め込みと出力投影の重みを結び付ける。
  • XNMTのデフォルトのハイパーパラメーターとドロップアウト(確率0.3)を使用して、Adamでトレーニング。
  • BPEサブワードを使用してOOVワードを処理。

コード: https://github.com/pmichel31415/mtnt

sacreBLEUを使って評価したところ、以下のスコアが出ています。 f:id:mathgeekjp:20190821105644g:plain

ドメイン適応のためのファインチューニングを行うとスコアが向上します。

f:id:mathgeekjp:20190821110004g:plain

考察

ノイズの多いテキストコンテンツは、言語タスクの主要なデータソースであるニュースコーパスと比較すると、ノイズが多い2

ニュースドメインには存在しないさまざまな言語現象が含まれており、モデルをドメイン外のデータに適用すると品質が低下するため、独自の課題がある3

これらの課題に対処する取り組みは、ドメイン内のデータセットアノテーションの作成4、およびドメイン適応トレーニングに焦点を合わせている。

NMTの具体的な課題は最近まで研究されなかった5。これらは、ソース文に単語内に自然ノイズまたは合成ノイズが含まれる場合の非自明な品質低下の経験的証拠を提供し、トレーニングデータにノイズを効率的に追加して堅牢性を向上させるデータ増大と敵対的アプローチを探っている。

MTNTの意義は、同時代のソーシャルメディアからのノイズの多い入力テキストに関連する幅広い現象を示す自然に発生するテキストで構成されるオープンテストセットを提供することにより、以前の研究と区別することにある。

まとめ

  • インターネット上の自然言語で発生するノイズの種類に対する堅牢性についてMTモデルをテストするための新しいデータセットがMTNT。
  • 英語・フランス語と英語・日本語の2つの言語ペア、およびこれら3つの言語の単一言語データの両方向の並列トレーニングとテストデータを提供している。
  • このデータセットには、既存のMTテストセットよりも多くのノイズが含まれており、標準のMTコーパスでトレーニングされたモデルに課題があることを示している。
  • これらの課題は、単純なドメイン適応アプローチだけでは克服できないことを示している。
  • MTNTは、MTのノイズに対する堅牢性の標準ベンチマークを提供し、この特定の問題に合わせたモデル、データセット、評価指標に関する研究を促進することを目的としている。

参考

tensor2tensorの翻訳モデルをサーバ化して実行

tensor2tensorで訓練した翻訳モデルを使いたいとき、複雑なコードを書かずに実行する方法の一つがserving1です。

実行方法

tensor2tensorですでに訓練済みの翻訳モデルが存在しているとします。

1. serving用にexportする (t2t-exporter)

t2t-exporterを使えば、serving用にエクスポートできます。

t2t-exporter --data_dir=data/data2 --problem=translate_jpen --model=transformer --hparams_set=transformer_base_single_gpu --output_dir=training_result2 --decode_hparams="beam_size=4,alpha=0.6" --t2t_usr_dir=.

2. tensorflow_model_serverを立ち上げる

tensorflow_model_server2は、tensorflowのモデルをサーバ化して使うためのモジュールです。

インストール方法は以下です。

echo "deb [arch=amd64] http://storage.googleapis.com/tensorflow-serving-apt stable tensorflow-model-server tensorflow-model-server-universal" | sudo tee /etc/apt/sources.list.d/tensorflow-serving.list && \
curl https://storage.googleapis.com/tensorflow-serving-apt/tensorflow-serving.release.pub.gpg | sudo apt-key add -
apt-get update && apt-get install tensorflow-model-server

インストールしたら、exportしたt2tモデルを読み込んで指定したポートで立ち上げます。

tensorflow_model_server --port=9000 --model_name=my_model --model_base_path=/root/work/mt/model2/training_result2/export/ &

3. pythonからAPIを使う (query.py)

from __future__ import absolute_import, division, print_function

import os

from six.moves import input
from tensor2tensor.serving import serving_utils
from tensor2tensor.utils import hparam, registry, usr_dir


def make_request_fn(servable_name="my_model", server="localhost:9000"):
    request_fn = serving_utils.make_grpc_request_fn(
        servable_name=servable_name, server=server, timeout_secs=30)
    return request_fn


def translate(text,
              udir="/root/work/mt/model2",
              ddir="/root/work/mt/model2/data/data3",
              servable_name="my_model",
              server="localhost:9000"):
    usr_dir.import_usr_dir(udir)
    problem = registry.problem("translate_jpen")
    hparams = hparam.HParams(
        data_dir=os.path.expanduser(ddir))
    problem.get_hparams(hparams)
    request_fn = make_request_fn(servable_name, server)
    inputs = text
    outputs = serving_utils.predict([inputs], problem, request_fn)
    outputs, = outputs
    output, score = outputs
    return {"input": text, "output": output, "score": score}


def run():
    while True:
        inputs = input(">>")
        result = translate(inputs)
        output = result["output"]
        score = result["score"]
        if len(score.shape) > 0:
            print_str = """
Input:
{inputs}

Output (Scores [{score}]):
{output}
        """
            score_text = ",".join(["{:.3f}".format(s) for s in score])
            print(
                print_str.format(inputs=inputs,
                                 output=output,
                                 score=score_text))
        else:
            print_str = """
Input:
{inputs}

Output (Score {score:.3f}):
{output}
        """
            print(print_str.format(inputs=inputs, output=output, score=score))


if __name__ == "__main__":
    run()

translateという関数を呼び出すことにより翻訳が実行できます。udirはユーザディレクトリで、tensor2tensorのユーザ定義問題を定義したファイルの場所です。ddirは、t2tの実行時に指定したデータディレクトリです。servable_nameはサーバ上で実行しているモデル名で、serverはサーバのアドレスです。

ちなみに、runは対話的に翻訳を実行するためのものですが、本質的に重要なのはtranslate関数です。

参考

warc_crawler: warcファイルでWebをクロール

Web ARChive(WARC)1アーカイブ形式は、複数のデジタルリソースを関連情報とともに集約アーカイブファイルに結合する方法です。 WARC形式は、インターネットアーカイブのARCファイル形式の改訂版で、World Wide Webから収集したコンテンツブロックのシーケンスとして「ウェブクロール」を保存するために伝統的に使用されてきました。WARC形式は、古い形式を一般化して、アーカイブ組織の収集、アクセス、および交換のニーズをより適切にサポートします。 現在記録されているプライマリコンテンツの他に、割り当てられたメタデータ、短縮された重複検出イベント、後の日付の変換など、関連するセカンダリコンテンツに対応します。

github.com

概要

bitextor2のようなツールでは、クロールしたデータをwarc形式で保持しています。そのため、自前の外部クローラを使う場合、warcで出力すれば、bitextorに読み込むことが可能です。

warc_crawler

warc_crawler3というクローラを作ってみましたが、これはWebをwarc形式で保存しながらクロールするツールです。実行フローは以下のようになっています:

  1. urls.txtを読み込む。
  2. urls.txtをスレッド数だけ分割する。
  3. それぞれのスレッドに分割されたurls.txtの最初のURLを読み込む。最初のURLに対応するwarcファイルが存在する場合は次を読み込む。
  4. wgetでwarcファイルとしてミラーを取得。timeoutも指定。
  5. warcファイル内に存在するすべてのホスト名を各スレッドのurls.txtに追加する。
  6. 3へ戻る。

コード

extract_links.py

warcからリンクを抽出するモジュール。

import re

regex = re.compile(r"https?://(?:[-\w.]|(?:%[\da-fA-F]{2}))+")
regex2 = re.compile(r"https?://")


def extract(text):
    return {re.sub(regex2, "", x) for x in re.findall(regex, text)}


def extract_from_warc(warc_file):
    with open(warc_file, encoding="utf-8", errors="ignore") as f:
        urls = extract(f.read())
    return urls

warc_crawler.py

クローラ本体。

import os
import subprocess
import pymp
import extract_links as el
from shutil import copyfile

template_wget = 'timeout 100 wget --mirror -4 -q {HOST} -P {DPATH} --warc-file "{WPATH}/{HOST}" -A.html -o wget.log'
template_sed = 'sed -i \'1d\' "{}"'


def wget(host, data_path="data", warc_pach="warcs"):
    cmd = template_wget.format(HOST=host, DPATH=data_path, WPATH=warc_pach)
    p = subprocess.Popen(cmd, stdout=subprocess.PIPE, shell=True)
    (output, err) = p.communicate()
    p_status = p.wait()
    return output, err


def update_urlfile(urlfile, urls):
    first_line = False
    with open(urlfile, "a") as f:
        f.write('\n'.join(list(urls)))
    with open(urlfile) as f:
        for line in f:
            first_line = line.strip()
            break
    cmd = template_sed.format(urlfile)
    p = subprocess.Popen(cmd, stdout=subprocess.PIPE, shell=True)
    (output, err) = p.communicate()
    p_status = p.wait()
    return first_line


def process(i, urls, tmp_dir, url_prefix, data_path, warc_path):
    urlfile = os.path.join(tmp_dir, url_prefix + str(i) + ".txt")
    first_line = update_urlfile(urlfile, urls)
    print(first_line)
    warc_file = os.path.join(warc_path, first_line + ".warc")
    if not os.path.isfile(warc_file):
        wget(first_line, data_path, warc_path)
        urls = el.extract_from_warc(warc_file)
    else:
        process(i, [], tmp_dir, url_prefix, data_path, warc_path)
    return urls


def crawl(tmp_dir="tmp",
          url_prefix="urls",
          data_path="data",
          warc_path="warcs",
          n_thread=8):
    urlfile = url_prefix + ".txt"
    for i in range(n_thread):
        copyfile(urlfile, os.path.join(tmp_dir, url_prefix + str(i) + ".txt"))
    urls_list = [[] for i in range(n_thread)]
    with pymp.Parallel(n_thread) as p:
        while (True):
            for index in p.range(0, n_thread):
                urls_list[index] = process(index, urls_list[index], tmp_dir,
                                           url_prefix, data_path, warc_path)


if __name__ == "__main__":
    crawl()

Note: 初期urls.txtは、スレッド数以上のurlを記述する必要があります。

参考

webvtt形式のパラレルコーパスのalignment

webvtt1は字幕データの一般的なファイル形式です。

概要

youtube-dl2を使って字幕データのパラレルコーパスを生成した場合、そのままの状態では1対1の文のペアにはなっていません。そこで、なんらかの方法でalignmentします。

youtube-dlでは、以下のコマンドで字幕を収集できます。

youtube-dl -ciw --all-subs --skip-download YoutubeチャンネルURL

コード

align.py

# coding: utf-8
import webvtt
import re
import MeCab
 
re_spc = re.compile(r"[ ]+")
tagger = MeCab.Tagger("-Owakati")
 
 
def tokenizer(text, tagger=tagger):
    return tagger.parse(text).split()
 
 
def time_second(time_str):
    time_data = list(map(float, time_str.split(":")))
    total = 0
    for i, t in enumerate(time_data):
        total += t * (60**(2 - i))
    return total
 
 
def length_score(text1, text2, tokenizer1, tokenizer2):
    s1 = len(tokenizer1(text1))
    s2 = len(tokenizer2(text2))
    diff_word = abs(s1 / (s1 + s2 + 1) - s2 / (s1 + s2 + 1))
    return 1 / (diff_word + 1)
 
 
def cross_inner(last_j, i, d1, data2, data2_arrived, threshold, tokenizer1, tokenizer2):
    d1_s = time_second(d1.start)
    d1_e = time_second(d1.end)
    try:
        data2[last_j]
    except IndexError:
        return None, None, False
    data = data2[last_j:]
    for j, d2 in enumerate(data):
        if last_j + j in data2_arrived:
            continue
        d2_s = time_second(d2.start)
        d2_e = time_second(d2.end)
        if (d1_s <= d2_s and d2_s <= d1_e) \
           or (d2_e <= d1_e and d1_s <= d2_e):
            data2_arrived[last_j+j] = True
            text1 = re.sub(re_spc, ' ', d1.text.replace("\n", " "))
            text2 = re.sub(re_spc, ' ', d2.text.replace("\n", ""))
            last_j = last_j + j + 1
            if length_score(text1, text2, tokenizer1,
                            tokenizer2) > threshold:
                return [text1, text2], last_j, True
            else:
                return None, last_j, False
        elif d2_s > d1_e:
            last_j = last_j + j + 1
            return None, last_j, False
    try:
        data2[last_j+1]
        return None, last_j+1, False
    except IndexError:
        return None, None, False
 
    
def cross(data1,
          data2,
          threshold=0.65,
          tokenizer1=tokenizer,
          tokenizer2=tokenizer):
    out = []
    data2_arrived = {}
    last_j = 0
    for i, d1 in enumerate(data1):
        row, last_j, flag = cross_inner(
            last_j, i, d1, data2, data2_arrived, threshold,
            tokenizer1, tokenizer2)
        if last_j is None:
            break
        if flag:
            out.append(row)
    return out

align_example.py

# coding: utf-8
 
import os
import align
import webvtt
from tqdm import tqdm
 

def run(files_en, files_ja):
    out = []
    for file_en, file_ja in tqdm(zip(files_en, files_ja)):
        try:
            data1 = [x for x in webvtt.read(file_en)]
            data2 = [x for x in webvtt.read(file_ja)]
            out += align.cross(data1, data2)
        except FileNotFoundError:
            print(file_en)
    with open("en-ja_all.txt", "w") as f:
        f.write('\n'.join(['\t'.join(x) for x in out]))
    return out
 
if __name__=="__main__":
    files = list([x for x in os.listdir("data_ted") if x.endswith("vtt")])
    titles_ja = {
        '.'.join(x.split(".")[:-2])
        for x in files if x.split(".")[-2] == "ja"
    }
    titles = sorted(list(titles_ja))
    files_en = [os.path.join("data_ted", x + ".en.vtt") for x in titles]
    files_ja = [os.path.join("data_ted", x + ".ja.vtt") for x in titles]
 
    run(files_en, files_ja)

アルゴリズムの説明

言語Aのある字幕Xと言語Bのある字幕Yがあったとき、この2つの字幕の時間が少しでも重なった場合、ペアの候補にします。ペアの候補は、length-based手法によってさらに絞り込まれます。

抽出結果の一部

Taking it a step further,    もう少し踏み込むと
MIT's media lab is working on robots    MITのメディアラボはもっと
that can interact more like humans. 人間のようにやり取りできるロボットを作っていますが
But what if those robots were able to interact  これらが特定の人物の
based on the unique characteristics of a specific person    独特の性格に基づいて対話することができたら?
based on the hundreds of thousands of pieces of content その人物が生前作った
that person produces in their lifetime? 何十万ものコンテンツをもとすることができたら?
Finally, think back to this famous scene    最後に 米国の2008年選挙の夜の
from election night 2008    この有名なシーンを
back in the United States,  思い出してください
where CNN beamed a live hologram    アンダーソン・クーパーとのインタビューのため
of hip hop artist will.i.am into their studio   CNNがヒップホップアーティストの
for an interview with Anderson Cooper.  will.i.amをホログラムでライブ中継しました
What if we were able to use that same type of technology    同じタイプの技術を使って
to beam a representation of our loved ones into our living rooms -- 愛する人たちの映像を自分の居間に映し それが
interacting in a very lifelike way  生前に作ったコンテンツをもとに
based on all the content they created while they were alive?    まるで生きているかのように話せたらどうでしょう?
I think that's going to become completely possible  私たちが使っているデータの量と
as the amount of data we're producing   それを理解する技術が両方
and technology's ability to understand it   飛躍的に拡大するにつれて
both expand exponentially.  これは全く可能となると思います
Now in closing, I think what we all need to be thinking about   最後に これが現実になって欲しいのか
is if we want that to become our reality -- もしそうなら
and if so,  これが人生やその後すべてに対し
what it means for a definition of life and everything that comes after it.  どんな意味があるのか考える必要があると思います
Thank you very much.    ありがとうございました

考察

ルールベースの場合、alignmentするために訓練のための追加のパラレルコーパスが必要になることがありません。ただし、精度があまり良くないかもしれません。TEDの場合は、全体的にほとんど1対1に近いかたちになっているので問題が起きにくいですが、もっと雑な翻訳者の作った字幕の場合、前述の手法では対処が難しいです。

例えば、字幕の表示時間が言語ごとに異なっていたりした場合は、一致する時間を基準に探すアルゴリズムが無駄になる可能性があります。

TEDの翻訳者は、各々の言語において自然な翻訳がなされるようにしているようなので、SVO形式の文法と、SOV形式の文法のペアでは、表示が前後する可能性があります。この解決策の一つは、TEDの英文は、英語ではコロンとピリオドがちゃんと使われていて、文の最初は大文字になっているので、この事実を用いて「文」の単位に修正することです。この場合、文全体では意味的に通るようになるので、語順の前後の問題が回避しやすくなります。

追記

alignmentを文ごとに生成するように修正しました。

[align.py]

# coding: utf-8
import webvtt
import re
import MeCab
 
re_spc = re.compile(r"[ ]+")
 
 
def time_second(time_str):
    time_data = list(map(float, time_str.split(":")))
    total = 0
    for i, t in enumerate(time_data):
        total += t * (60**(2 - i))
    return total
 
 
def cross_inner(last_j, i, d1, data2, data2_arrived):
    d1_s = time_second(d1.start)
    d1_e = time_second(d1.end)
    try:
        data2[last_j]
    except IndexError:
        return None, None, False
    data = data2[last_j:]
    for j, d2 in enumerate(data):
        if last_j + j in data2_arrived:
            continue
        d2_s = time_second(d2.start)
        d2_e = time_second(d2.end)
        if (d1_s <= d2_s and d2_s <= d1_e) \
           or (d2_e <= d1_e and d1_s <= d2_e):
            data2_arrived[last_j+j] = True
            text1 = re.sub(re_spc, ' ', d1.text.replace("\n", " "))
            text2 = re.sub(re_spc, ' ', d2.text.replace("\n", ""))
            last_j = last_j + j + 1
            return [text1, text2], last_j, True
        elif d2_s > d1_e:
            last_j = last_j + j
            return None, last_j, False
    try:
        data2[last_j+1]
        return None, last_j+1, False
    except IndexError:
        return None, None, False
 
    
def cross(data1, data2):
    out = []
    data2_arrived = {}
    last_j = 0
    for i, d1 in enumerate(data1):
        row, last_j, flag = cross_inner(
            last_j, i, d1, data2, data2_arrived)
        if last_j is None:
            break
        if flag:
            out.append(row)
    return out

[align2]

from tqdm import tqdm
 
ALPHA = "abcdefghijklmnopqrstuvwxyz"
 
ALPHA_DICT = {c: True for c in list(ALPHA) + list(ALPHA.upper())}
 
 
def merge_sents(data_file):
    out = []
    with open(data_file) as f:
        current_en = []
        current_ja = []
        for line in tqdm(f):
            line = line.strip().split("\t")
            c = None
            i = 0
            while (c not in ALPHA_DICT and i<len(line[0])):
                c = line[0][i]
                i += 1
            if i >= len(line[0]):
                continue
            if c.isupper():
                entxt = ' '.join(current_en)
                jatxt = ''.join(current_ja)
                out.append(entxt + "\t" + jatxt)
                current_en = []
                current_ja = []
            current_en.append(line[0])
            current_ja.append(line[1])
        if current_en and current_ja:
            entxt = ' '.join(current_en)
            jatxt = ''.join(current_ja)
            out.append(entxt + "\t" + jatxt)
    return out
 
 
if __name__ == "__main__":
    out = merge_sents("./data/TED/en-ja_all.txt")
    with open("./en-ja_fixed.txt", "w") as f:
        f.write('\n'.join(out))

参考

youtube-dlを使ってYoutube動画の字幕からパラレルコーパスを生成

youtube-dl1は、youtube動画をダウンロードするためのCLIツールです。

インストール

Unix系ユーザーは以下のコマンドでインストールが可能です。

sudo curl -L https://yt-dl.org/downloads/latest/youtube-dl -o /usr/local/bin/youtube-dl
sudo chmod a+rx /usr/local/bin/youtube-dl

pipも使えます。

sudo -H pip install --upgrade youtube-dl

字幕だけをダウンロードする

youtube-dlはたくさんのオプションがありますが、字幕をダウンロードするオプション2もあります。

youtube-dl --all-subs --skip-download https://www.youtube.com/watch?v=1mLQFm3wEfw

--skip-downloadオプションにより、動画本体のダウンロードをスキップできます。これにより、字幕だけがダウンロードされます。

生成されたファイルの例

前述のコマンドを実行すると、以下のファイルが生成されます。

動画タイトル-動画ID.言語.vtt 

生成されたファイルの中身は以下のようになっています:

WEBVTT
Kind: captions
Language: pl
 
00:00:00.000 --> 00:00:07.000
Tłumaczenie: Barbara Guzik
Korekta: Marta Konieczna
 
00:00:13.407 --> 00:00:14.988
Jeśli istnieje miasto,
 
00:00:15.012 --> 00:00:17.635
w którym trudno znaleźć lokum
do kupienia lub wynajęcia,
 
00:00:17.675 --> 00:00:18.829
to musi być Sydney.
 
00:00:19.043 --> 00:00:21.410
Jeśli ostatnio próbowaliście
znaleźć tu dom,
 
00:00:21.434 --> 00:00:23.274
to wiecie, o czym mówię.
 
00:00:23.298 --> 00:00:25.342
Zawsze kiedy wchodzicie
do domu na sprzedaż,
 
00:00:25.362 --> 00:00:27.527
jesteście informowani o różnych ofertach

チャンネルの動画を全体的に取得する

動画一個一個を取得するのは面倒ですが、チャンネルからすべての動画の字幕を取得する方法があります。

youtube-dl  -ciw --all-subs --skip-download <url-of-channel>

例えば、TEDのようなチャンネルを入力すれば、多くの言語のパラレルコーパスが作成できます。動画そのものをダウンロードすることもできますが、このブログは主に自然言語処理について書いているので割愛します。

参考

bitextor: Webから自動的にパラレルコーパスを生成するツール

bitextor1とは、指定したホストから自動的にパラレルコーパスを収集するツールです。

概要

bitextorは、paracrawlプロジェクト2で使われているツールで、Webから自動的にパラレルコーパスを生成します。機械翻訳の大きな課題は、大量かつ質の良いデータを用意することですが、Webという巨大な資源を利用すれば、人間の手をあまりかけずにコーパスを用意することができます。

実行方法

実行方法は色々ありますが、今回はdockerイメージを利用して実行します。

dockerコンテナに入る

docker pull paracrawl/bitextor
docker run -it paracrawl/bitextor

必要なものを入れる

cd /opt/bitextor
apt install cmake automake pkg-config python3 python3-venv python3-pip libboost-all-dev openjdk-8-jdk liblzma-dev time poppler-utils httrack 
pip3 install -r requirements.txt
pip3 install -r bicleaner/requirements.txt https://github.com/bitextor/kenlm/archive/master.zip
pip3 install -r bifixer/requirements.txt

トーカナイザの準備(mecab-tokenizer.perl)

次に、言語固有のトーカナイザを準備します。デフォルトでは、Mosesのトーカナイザを利用しているようですが、日本語を使いたいので、MeCabを使います。ただ、Mosesを使うようになっているため、perlしか実行できないのかもしれないので、perlMeCabのラッパーを作成します。(事前にMeCabのインストールが必要です。)

#!/usr/bin/env perl
system("mecab -Owakati");

sentence splitterも用意する必要がありますが、Mosesは日本語にも対応できるのでそれを使います。

パラレルコーパスの準備

document alignmentの方法をオプションで選択できますが、今回は辞書ベース手法を使うので、その辞書を作成するためのパラレルコーパスを準備します。

mkdir dic_data
cd dic_data
mkdir Tatoeba
cd Tatoeba
wget https://object.pouta.csc.fi/OPUS-Tatoeba/v20190709/moses/en-ja.txt.zip
unzip en-ja.txt.zip
gzip Tatoeba.en-ja.ja
gzip Tatoeba.en-ja.en

myconfig.yamlを書く

bitextorを実行する際に読み込む設定ファイルを書きます。

# BITEXTOR CONFIG FILE
 
bitextor: /opt/bitextor
temp: /home/bitextor/transient
permanentDir: /home/bitextor/permanent/bitextor-output
transientDir: /home/bitextor/transient
 
LANG1Tokenizer: /opt/bitextor/preprocess/moses/tokenizer/tokenizer.perl -q -b -a -l en
LANG2Tokenizer: /opt/bitextor/preprocess/moses/tokenizer/mecab-tokenizer.perl
LANG1SentenceSplitter: /opt/bitextor/preprocess/moses/ems/support/split-sentences.perl -q -b -l en
LANG2SentenceSplitter: /opt/bitextor/preprocess/moses/ems/support/split-sentences.perl -q -b -l yue
 
lang1: en
lang2: ja
 
crawler: wget
crawlTimeLimit: 30s
crawlSizeLimit: 1G
crawlTld: false
crawlerNumThreads: 8
crawlerConnectionTimeout: 10
 
hosts: ["mynlp.is.s.u-tokyo.ac.jp"]
 
dic: /opt/bitextor/dic_data/Tatoeba/en-ja.dic
initCorpusTrainPrefix: ["/opt/bitextor/dic_data/Tatoeba/Tatoeba.en-ja"]
boilerpipeCleaning: false
 
documentAligner: DIC

hostsには、クロール対象のホストを指定します。このツールは、指定されたホストのみを探索します。ホスト内のlang1とlang2が対応しているURLを見つけたら、そこからdocument alignmentを実行します。ホストは複数指定できますが、ホストファイルを用意することもできます。その場合は、以下のような設定パラメータを指定してください。

hostsFile: /home/user/hosts.gz

hosts.gzは、hostsというファイルに改行区切りでホストを記述したものをgzip圧縮したものです。

注意: 上記のhostsの設定をそのまま使わないでください。相手先のサイトに迷惑をかけます。

実行

以下を実行します。

./bitextor.sh -s myconfig.yaml

結果

抽出結果は、myconfig.yamlで記述された permanentDir 内に保存されます。

ls /home/bitextor/permanent/bitextor-output
en-ja.sent.xz  hosts.gz  warc

en-ja.sent.xzを展開すれば、抽出結果が見れます。抽出結果は以下の形式になっています。

src_url,tgt_url,src_sentence,tgt_sentence,score

Note: 実際はコンマではなく、タブ区切りです。

考察

bitextorでは、指定したホストのページだけを収集するので、Web全体というわけにはいきません。Web全体から探したい場合、hostsの候補URLを探す形でクローリングすることになりそうです。検索エンジンからinurl:enのようなオプションを利用して検索できれば理想的ですが、すぐに検索エンジン側からブロックされてしまうでしょう。

そのため、hostsファイルを複数に区切って連続的に生成するような仕組みが必要になります。さらに、生成されたhostsファイルから、bitextorで抽出していないものを探し出し、連続的に、あるいは複数のマシンによって実行するのがよいかもしれません。

要するに、実用的な面から言えば、bitextorを使う際に以下の2つのフェーズを実行する必要があります。

  • 候補となるホストの発見。
  • 発見されたホストに対してbitextorを実行。

ホストの発見のためのスクリプトはないので、自前で用意する必要があります。

ちなみに、bitextorを任意の言語で実行するために必要な最小限のリソースは以下です:

  • トーカナイザ。
  • センテンススプリッタ。
  • 指定したdocument alignerが必要とするもの(パラレルコーパス、辞書、MTシステムなど)

これは、開発者によってissue3で話されています。

追記

crawlTld: creepy-specific option that allows the crawler to jump to a different web domain as far as it is part of the same top-level domain (TLD); a TLD could be, for example, .es, .info or .org

「同じトップレベルドメイン内であれば、他のwebサイトへも飛ぶ」というオプションを有効化できるようです。なので、ホストの発見フェーズをつけなくても使えます。

参考