エイエイレトリック

なぐりがき

アルファベットをカタカナに変換するpythonパッケージalphabet2kanaを公開しました

f:id:sh111h:20210328123716p:plain

TL;DR

ありそうでなかった、アルファベットをカタカナに変換するや〜つを作りました。*1

github.com

ABCエービーシー に変換します。 読みの付与に使うことを目的としています。

pypi にも登録したので pip でインストールできます。

pip install alphabet2kana

ぜひ使ってみてください。

機能

jaconv の実装に習って a2k という関数で変換できます。

Aエー といったように、単純変換です。 ローマ字読みは しません 。(下に記載の関連パッケージを使うとよいです)

from alphabet2kana import a2k

a2k("ABC")
# エービーシー

日本語が混じっていても使えるようになっています。

a2k("SOS団")
# エスオーエス団

a2k("aclは言語処理だけでなくAFCチャンピオンズリーグの略でも使われます")
# エーシーエルは言語処理だけでなく
# エーエフシーチャンピオンズリーグの略でも使われます

因みに半角のアルファベットにのみ対応しているので、全角アルファベットは半角にする前処理が必要です。 さくっと前処理したい場合はmojimojijaconv を使ってください。

unicodedata.normalize("NFKC")で正規化してもいいと思います。

区切り文字 (delimiter) の機能もあります。

a2k("SOS団", delimiter="・")
# エス・オー・エス団

経緯

mecab の辞書ipadic や Sudachi の辞書にはアルファベットの1文字の読みが付与されておらず、 読みを出力すると* になってしまうことに気づきました。

とはいえ、漢字と違い、アルファベットの読みはほぼ一意に定まるので「とりあえず読みが欲しい」ときの処理ができたらいいなと思い、つくってみました。

Unidic にアルファベットの読み情報があったので、カタカナ表記はUnidicをベースにしています。Zだけ ゼットズィー の2通りの可能性があるのですが、単体での読みは ゼット が多いと考え、 ゼット にしました。

a2k("マジンガーZ")
# マジンガーゼット

a2k("Zホールディングス")
# ゼットホールディングス

関連パッケージ

python-romkan

github.com

  • ローマ字とアルファベットの変換
  • アルファベットをローマ字読みに変換する機能 romkan.to_katakana があります
    • ローマ字表記ではない場合、英字が残ります
 import romkan

romkan.to_katakana("esuouesu")
# エスオウエス

romkan.to_katakana("SOS")
# ソs

alkana.py

github.com

  • 英単語をカタカナに変換
  • 入力はアルファベットのみ受け付けます
import alkana

alkana.get_kana("Hello")
# ハロー

alkana.get_kana("SOS")
# エスオーエス

alkana.get_kana("SOS団")
# None

poetryを使ってpypiに登録する

はじめてpythonパッケージを公開するにあたって、以下の記事を参考にしました。 poetryで環境構築した場合、 poetry publish だけで公開できるので、とても便利だと思いました。

*1:このブログでアルファベットは 英語アルファベット を指します

scikit-learnにmecab日本語分かち書きを組み込む方法

pythonで (深層学習ではない) 機械学習をするとなるとまず使うのがscikit-learn、さらに扱うデータがテキストとなると大体の場合 CountVectorizerや TfIdfVectorizer といった特徴量抽出 feature_extraction.text で前処理を行うことが定番だと思います。

しかし、これらの Vectorizer は日本語のような文節がない言語向けにつくられているわけではありません。

デフォルトの設定 analyzer="word" だとスペース区切りの分割が実行されるため、日本語の場合はmecabなどで分割する前処理が必要になります。

やり方としては大きく3つ、それぞれ例示してみます。

だいたい mecab-python3 を使う方法で書いていますが、fugashi・Sudachi・PyKNPといったトークナイザー・形態素解析器に置き換えても実行できるはずです。

使ったパッケージ・依存関係は以下の通りです。

python = "^3.7"
mecab-python3 = "^1.0.3"
scikit-learn = "^0.24.1"

1. 分かち書きしてからVectorizerに渡す

英語と同じように単語ごとスペースを入れておけば問題ないので、前処理として実行する。一番単純な方法です。

具体的には、sklearnを実行する前に MeCab.Tagger.parse を実行します。

gist.github.com

※「も」「の」が feature_names にないのはCountVectorizerがデフォルトで1文字単語を除外しているからです。 token_pattern が default=r"(?u)\b\w\w+\b" なので (?u)\b\w+\b に修正すると1文字もvocabに採用されます。

もしくはmecabコマンドの実行結果を元ファイルとは別に保存しておくという選択肢もあるかもしれません。

今となってはコーパスは生文で配布されていることが大半ですが、形態素解析済みで配布されているコーパスも存在します。NLTK Japanese Corpora - NLTKで使える日本語コーパス で紹介されているコーパスがこれにあたります。

あらかじめ形態素解析の結果を別ファイルで保存しておけば、毎回分かち書きを実行しなくて済むためモデルだけに注力でき、再現性が担保されるメリットがあります。

ですが、どうやって分かち書きしたか(どのトークナイザーか、どの辞書か)の情報は一緒に保存しないことが多いのできちんと記録・共有しておくのを忘れないよう注意しなくてはいけません。

最悪の場合、現状分かち書きされているファイルでしかモデルが実行できないとか、そういった類の問題が起こってしまう危険性があります。

1行1文書のデータに対してshellスクリプト分かち書きを設定するとしたら以下のような感じになりそうです。

#!/bin/sh
while read row; do
  text=`echo ${row} | cut -d , -f 1`
  echo "$text" | mecab -Owakati  >> $2
done < $1

参考

2. Vectorizerのanalyzerに渡す

処理機構がバラバラにならないよう、なるべくsklearnと一緒に使いたいと思うかもしれません。

実はVectorizer系のsklearnクラスの引数 analyzer が使えます。ドキュメントには

If a callable is passed it is used to extract the sequence of features out of the raw, unprocessed input.

https://scikit-learn.org/stable/modules/generated/sklearn.feature_extraction.text.CountVectorizer.html

と書いてあり、callableな関数も渡すことができることがわかります。 _analyze の実装見る限り analyzer の代わりにtokenizer に渡しても問題ないと思われます。

github.com

サンプルコード

mecab-python3の分かち書き結果を返す関数を設定して analyzer に渡す。

gist.github.com

mecab_analyzer によってCountVectorizerの設定として組み込めました。analyzer="word" の設定を上書きしているので1文字単語も語彙に登録されます。

analyzerに渡す関数を変更すれば名詞だけにしたり単語を原形で渡したりできます。

ひとつ問題になるのがこのVectorizer vec と一緒にmecabの情報を保存できないという点。

mecab-python3はあくまで(C++で書かれた)mecabpython wrapperなので、トークナイザの情報は保存できないです。

そのため、pickleで保存したモデルを別ファイルで読み込むとエラーになってしまいます。

応急処置っぽくはなりますが、 mecab_analyzer 関数を読み込むファイルと同じファイルに設定しておけば読み込みは可能です。

import pickle

# mecab_analyzer がないとエラー
# AttributeError: Can't get attribute 'mecab_analyzer' on <module '__main__' from 'load_vectorizer.py'>
import MeCab

mecab = MeCab.Tagger("-Owakati")


def mecab_analyzer(text):
    return mecab.parse(text).split()


texts = [
    "すもももももももものうち",
    "スモモもモモもモモのうち",
    "李も桃も桃のうち",
]

with open('outputs/sample.pickle', 'rb') as f:
    vec_new = pickle.load(f)
result = vec_new.fit_transform(texts)
print(result.toarray())
print(vec_new.get_feature_names())

参考

3. 分かち書きをする自作クラスを作る

pipelineで分かち書きを含めたモデルを作りたい、pickleで保存したい、様々な設定の分かち書きで実験したい、そんな場合は自作クラスを作るのがよさそうです。

sklearnのBaseEstimator を継承して、他のsklearnクラスと同様 fittransformfit_transform を設定すれば自作クラスとして機能します。

前述の通り、pickle化できるようにはクラスに工夫が必要になります。

自分で設定するのが面倒な人はサンプルコードを作ったので以下のコードを改変して使ってください。

pickle化するには

mecabのpickle化について、解説している記事がいくつかあるのでこれらを参考にします。

要するに、自作クラスのcallable object から Tagger オブジェクトをpickle化する時に外して、非pickle化の時に渡しなおせばよいということです。

具体的には__reduce_ex__ の設定をいじります。 __reduce__ に書いてある通りのタプルを返せばよいのですが、2番目の

呼出し可能オブジェクトに対する引数のタプル。呼出し可能オブジェクトが引数を受け取らない場合、空のタプルが与えられなければなりません。

で CountVectorizerにおけるanalyzer の情報 以外 を渡すようにします。

ただし、情報をはずしても__init__Taggerを呼び出せるような引数を設定しておく必要があります。 def __init(self, **kwargs) のような可変長引数で受け取る設定にしているとうまくいかない場合があります。

また、pickleを読み込む時に自作クラスがimportされていないと analyzer の方法と同様やはりエラーになるので注意です。

pip installできるモジュールにしておけばimportは必要ないと思うのですが、詳細は未確認です。。

サンプルコード

gist.github.com

これで mecab-python3 によってトークナイズしていることに加え辞書のパス情報もpickleで保存することができています。

辞書のパスが保存できるということは、OSが異なったり辞書の保存先をカスタマイズしていたりするとpickleを読み込む時にエラーになります。 あくまで同じ端末で使うために使う目的の自作クラスです。

まとめ

  1. 別々で実行 simple_mecab_sklearn.py · GitHub
  2. analyzerに渡す mecabをCountVectorizerのanalyzerに渡す · GitHub
  3. 自作クラスをつくる owakati_tokenizer.py · GitHub

個人的には2か3にしたいのですが、 「CountVectorizer はanalyzer設定含んでるクラスなのに、別クラスで分かち書きを設定するのは気持ち悪いな〜」とか、「analyzerに関数渡すの場合思いがけない設定に影響出るからな〜」とか考え方は色々あると思うので、最後は好みの問題になると思ってます。

一長一短でベストプラクティスがなかなか見つからないので、他に方法があれば教えてください。絶賛募集中です。

EMNLP2020読んだ論文メモ

EMNLP2020の論文を去年から少しずつ読んでいたので、メモをまとめてブログにあげます。

2020.emnlp.org

気になった論文をチョイスしているので、いつもはタスク提案・メタ分析・固有表現 (NER) の論文を選ぶことが多くなってしまうのですが、今のご時世もあって、医療ドメイン系の論文も多めに読んでます。

医療ドメインやファクトチェック関連の論文の中には COVID-19 について言及している論文も多くあり、自然言語処理の実世界での活用が重要視されている流れを感じました。

紹介というより簡単なメモなので、詳細は論文を読んでください。 Proceedings of the 2020 Conference on Empirical Methods in Natural Language Processing (EMNLP) - ACL Anthology の順番に紹介します。

A matter of framing: The impact of linguistic formalism on probing results

https://www.aclweb.org/anthology/2020.emnlp-main.13/

Repulsive Attention: Rethinking Multi-head Attention as Bayesian Inference

https://www.aclweb.org/anthology/2020.emnlp-main.17/

FIND: Human-in-the-Loop Debugging Deep Text Classifiers

https://www.aclweb.org/anthology/2020.emnlp-main.24/

Event Extraction by Answering (Almost) Natural Questions

https://www.aclweb.org/anthology/2020.emnlp-main.49/

  • event/argument extractionはentity抽出に重きを置いていたり、relationラベルが似ているのが原因でneural modelは弱かった
  • QAタスクとして解くことでentityやrelationの制約を克服する
  • zero-shot learningでも解く
  • github https://github.com/xinyadu/eeqa

f:id:sh111h:20210117174305p:plain

Joint Constrained Learning for Event-Event Relation Extraction

https://www.aclweb.org/anthology/2020.emnlp-main.51/

SetConv: A New Approach for Learning from Imbalanced Data

https://www.aclweb.org/anthology/2020.emnlp-main.98

  • 不均衡データに対し、データの畳み込みをする前処理レイヤーを追加する
  • 少数派のクラスからanchor dataを設定する

Learning from Task Descriptions

https://www.aclweb.org/anthology/2020.emnlp-main.105/

  • zero-shot learningの拡張。タスクの 説明 も学習に組み込めるようなデータセットの構築。
  • 1つのタスク説明に対して複数の context (文) があり、説明の通りに解いて回答する。
    • e.g.「この公園は登山可能ですか?」という説明と、公園についての文が複数個与えられて、「登山可能か」それぞれyes/no/NAで答える
  • dataset https://allenai.org/data/zest

f:id:sh111h:20210117173554p:plain:w600
データ例

Explainable Clinical Decision Support from Text

https://www.aclweb.org/anthology/2020.emnlp-main.115

  • 構造化されていないclinical text (electronic medical records) からBERTやattentionモデルを使ってマルチタスクを解く
  • 汎用BERTよりも医療ドメインのBERT使った方が精度はよい

Knowledge Graph Alignment with Entity-Pair Embedding

https://www.aclweb.org/anthology/2020.emnlp-main.130/

Querying Across Genres for Medical Claims in News

https://www.aclweb.org/anthology/2020.emnlp-main.139

f:id:sh111h:20210117174648p:plain:w450

Improving Grammatical Error Correction Models with Purpose-Built Adversarial Examples

https://www.aclweb.org/anthology/2020.emnlp-main.228

  • 文法誤り修正 (GEC) はseq2seqが一般的だがデータの量と質に依存している
  • adversarial データを作り、probability, attention weightを使ってweak spotを探し、他の単語と置き換える
  • やっぱり前置詞は難しい

f:id:sh111h:20210117175311p:plain:w450
Figure 1より

Sound Natural: Content Rephrasing in Dialog Systems

https://www.aclweb.org/anthology/2020.emnlp-main.414/

  • virtual assistantでの利用を例に、発話の言い換えを考える rephrasing task。
  • データセットの作成
    • e.g. Let Kira know I can pick her up -> I can pick you up

Interpretable Multi-dataset Evaluation for Named Entity Recognition

https://www.aclweb.org/anthology/2020.emnlp-main.489

f:id:sh111h:20210117174042j:plain
github READMEより

Simple and Effective Few-Shot Named Entity Recognition with Structured Nearest Neighbor Learning

https://www.aclweb.org/anthology/2020.emnlp-main.516

  • few-shot learning でNER
  • 近傍分類をしてvitabiの遷移確率を使う Viterbi decoder

Fact or Fiction: Verifying Scientific Claims

https://www.aclweb.org/anthology/2020.emnlp-main.609/

f:id:sh111h:20210117173423p:plain:w450
Figure 1より

Named Entity Recognition Only from Word Embeddings

https://www.aclweb.org/anthology/2020.emnlp-main.723

Re-evaluating Evaluation in Text Summarization

https://aclweb.org/anthology/2020.emnlp-main.751/

  • 文書要約の評価方法を再評価する
  • 要約システムを自動評価する方法と人手による評価を比較
    • system-level での相関、summary levelでの相関
  • 評価指標はデータセットにも依存するので複数のデータセットで評価するのがよい
  • github https://github.com/neulab/REALSumm

Workshop 論文

The Extraordinary Failure of Complement Coercion Crowdsourcing

https://www.aclweb.org/anthology/2020.insights-1.17/

  • うまくいかなかった研究結果についてのワークショップ、Workshop on Insights from Negative Results in NLP の論文
  • I started a new book には started reading/writing を暗に示している。
  • このように文から意味を推定し動詞を補完する Complement Coercion 用のデータをクラウドソーシングで構築しようとした。
  • 自然言語推論 (NLI) タスクとして3択問題にしたところ一致率が非常に低かった。
  • 読み手によって construe が異なっていたためと考える
  • その他 NLI データセット構築の上での問題点

f:id:sh111h:20210117175706p:plain:w450
クラウドソーシングの結果

Sentence Boundary Detection on Line Breaks in Japanese

https://www.aclweb.org/anthology/2020.wnut-1.10/

f:id:sh111h:20210117175814p:plain:w450
データ例

おわりに

今年は752本あった論文、タイトルだけは一通り目を通したもののやっぱり多いですね。

来年は800超えるのでしょうか……。

blog.hoxo-m.com

Python「ytmusicapi 」で音楽をYoutube Musicにアップロードする

前置き 〜Google Play Musicのエクスポート〜

Google Play Music のサービスの年内終了が決まり*1、今月になって自分のアカウントも音楽を利用できなくなってしまいました。 長年音楽プレイヤーとして愛用していただけに残念です。

実は普段使っているGoogleアカウントと別のアカウントでGoogle Play Musicを使っており、これを機に一旦音楽をエクスポートして、普段使いのアカウントのYoutube Musicに移行することにしました。

家族で共用に使っていたHDDと同期していたこともあったので、エクスポートのデータ量がGoogle photoの無料保存容量を優に超えてました(※Updating Google Photos’ storage policy to build for the future)。 これだけあるとただのバックアップストレージになっている感は否めません。

エクスポートされるデータはmp3とは別に、再生回数などの情報を含んだcsvファイルも含まれていて、分割して出力した場合csvとmp3がランダムに混ざっているので絶妙に扱いにくいです。

f:id:sh111h:20201114145644p:plain
"https://takeout.google.com/settings/takeout" より

なぜか(?)曲ごとにcsvが出力されており、mp3 ファイルと同じフォルダに入っています。

f:id:sh111h:20201114145906p:plain
『バッハの旋律を夜に聴いたせいです。』.csv

分割ファイルごと、mp3ファイルだけ選択して Youtube Musicにアップロードするのも面倒ですし、Youtube Musicにはアップロード用のマネージャーは公開されてなさそうなので、コードを書いて解決します。

Youtube Musicにアップロードする公式な方法

念のため公式方法も記述しておきます。

Google Play Musicから Youtube Music への移行するツールは https://music.youtube.com/transfer に用意されています。

自分の端末からアップロードする場合、ブラウザ上で行うしかないようです。

support.google.com

ytmusicapi でYoutube Musicにアップロードする

アップロードを自動化したくなったので、Youtube Musicのapiを探したところ、「ytmusicapi」がヒットしました。

github.com

ytmusicapi: Unofficial API for YouTube Music

A work-in-progress API that emulates web requests from the YouTube Music web client.

と書いてある通り非公式のAPIなのですが、pip install で簡単にインストールできるpython パッケージなので使うことにしました。

以降、Python 3.7、 Mac OS Catalina で動作確認しています。 2020年11月時点で v0.10.2 は問題なく使えていますが、Youtube Musicの仕様変更によっては使えなくなる可能性があります。

初期設定

前述の通り、pip install ytmusicapi でインストールできます。

初期設定はほぼドキュメントのSetup通り行えばよいです。

headers_auth.json を作成もしくは生成する方法としては、大きく二つあります。

要求ヘッダー (Request Header) の情報を YTMusic.setup() に入力して json を生成するか、manual-file-creationの形式に従って手動でjsonを作成するかです。

Firefoxの場合、Raw (生ヘッダー) 設定をonにしたデータをコピペして使います。 Raw ではないとjsonにフォーマットされてしまい、setup() に入れるとエラーになるので注意してください。

f:id:sh111h:20201114172316j:plain
Firefoxの場合

保存したheader_auth.json の情報を YTMusic クラスで読み込むことで、自分のアカウントのYoutube Musicを操作できるというわけです。

フォルダー下にあるMP3ファイルをアップロードする

YTMusic.upload_song() でパスを指定してアップロードできるので、フォルダー下にある mp3を指定してアップロードするコードをさくっと書きました。 データ数が多いので tqdm を使ってプログレスバーを表示するようにしています。

実際に実行し、その後ライブラリの曲を「最近追加」でソートし、アップロードした曲があることを確認できました。

別途定期実行する仕組みを作れば、自動同期みたいなこともできそうです。

その他の機能

ytmusicapi: Unofficial API for YouTube Music — ytmusicapi 0.10.2 documentation を見る限り曲の検索やプレイリストの作成など、ブラウザ上でできる作業は一通り可能なようです。 データの操作機能がメインで、現時点で音楽の再生はできないです。

音楽データを整理整頓するときに便利なAPI、という立ち位置だと思います。

アップロードさくっとしたいのであれば、ytmusicapi を活用した以下のライブラリもあるので、こちらを使うのもいいかもしれません。

github.com

最後に

Youtube Musicの非公式API「ytmusicapi 」を使ってみたので紹介しました。 設定さえできれば簡単に使えるのでよかったです。

Youtube Musicという名前だけあって、あまり既存の音楽をアップロードして聴くためのサービスではないのかなとも薄々感じてるんですが、アップロードした音楽のアプリでのオフライン再生機能はあるみたいなので、後継サービスとして使ってみようと思います。

なんにせよ、Google Play Musicの終了は音楽がサブスク・動画ありきの時代になったことを実感させられます。

music.youtube.com

(Firefoxスクリーンショットに映りこんだやつ)

再訪Pythonチュートリアル

ある程度、他の言語を経験したことがある場合、新しい言語のチュートリアルは流し読みしても問題ないことが多いです。 かくいう自分もPythonチュートリアルをちゃんと読んでこなかったので、改めて精読してみました。

実際に読んでみると、結構気づきがあったので紹介しようと思います。 「こんなん知ってるわ」なものや「こんなん使わん」なものも多いかと思いますが、復習と思って書いています。

Python チュートリアル を参考にしています。

形式ばらない Python の紹介

https://docs.python.org/ja/3/tutorial/introduction.html

冪乗

pow だけでなく ** で冪乗ができる。

2**3
>> 8
pow(2,3)
>> 8

対話モード

対話モードでは、最後に表示された結果は変数 _ に代入されます。このことを利用すると、Python を電卓として使うときに、計算を連続して行う作業が多少楽になります。

tax = 12.5 / 100
price = 100.50
price * tax
>> 12.5625

# `_` には price*tax が代入されている
price + _
>> 113.0625

immutable/mutable

  • 文字列は immutable
  • リストは mutable

チュートリアルでは組み込みデータ型ごとにまとめた情報はない

  • 用語集には用語の意味が記載されている

(イミュータブル) 固定の値を持ったオブジェクトです。イミュータブルなオブジェクトには、数値、文字列、およびタプルなどがあります。これらのオブジェクトは値を変えられません。 immutable


(ミュータブル) ミュータブルなオブジェクトは、 id() を変えることなく値を変更できます。 mutable

参考

その他の制御フローツール

https://docs.python.org/ja/3/tutorial/controlflow.html

break

  • else は for に属する
for n in range(2, 10):
     for x in range(2, n):
         if n % x == 0:
             print(n, 'equals', x, '*', n//x)
             break
     else:
         # loop fell through without finding a factor
         print(n, 'is a prime number')

関数

デフォルト値

def foo(k,v, mydict={}):
    mydict[k] = v
    print(mydict)
 
 foo(1,2)
 >> {1: 2}
 foo(2,3)
 >> {1: 2, 2: 3}
 foo(1, 4)
 >> {1: 4, 2: 3}

mutableな型で影響させないためにはデフォルト値を None に設定する

 def foo(k,v, my_dict=None):
    if my_dict is None:
        my_dict = {}
    my_dict[k] = v
    print(mydict)

キーワード専用引数

  • * を前に置くと、その後ろの引数はキーワードでしか受け取ることができない。これをキーワード専用引数と呼ぶ
 def func(a, *, b):
    print(a,b)
 
 # NG
 func(1,2)
 >> TypeError
 
 f1(1,b=2)
 >> 1 2
  • 可変引数のあとの引数はキーワード専用引数
    • sep= の部分
 def concat(*args, sep="/"):
     return sep.join(args)
     
 concat("earth", "mars", "venus")
 >> 'earth/mars/venus'

 concat("earth", "mars", "venus", sep=".")
 >> 'earth.mars.venus'

引数のunpack

  • リストは *args、 辞書は **args でアンパックし渡すことができる
def func(a,b):
    print(a,b)
    
func(*[1,2])
>> 1 2
func(**{"a":1, "b":2})
>> 1 2

# NG
func([1,2])
>> TypeError: func() missing 1 required positional argument: 'b'

ラムダ式

データ構造

https://docs.python.org/ja/3/tutorial/datastructures.html

スタック・キュー

Pythonにはスタック (stack) やキュー (queue) という名前のデータ構造はないが、リストや deque によって実現できる。

リストをスタックとして使う

  • last-in, first-out
  • listappendpop を使う

リストをキューとして使う

  • first-in, first-out
  • collectionsのdequeappendpopleft を使う

タプル

  • 丸括弧をつけなくてもタプル扱い(unpackの逆)
  • singleton = 'hello',

浮動小数

https://docs.python.org/ja/3/tutorial/floatingpoint.html

機械翻訳が発展した時代で人間側ができる工夫方法あれこれ

機械翻訳で翻訳できないこととそれに対する(人間側の)解決策について書きます。

工夫方法とタイトルに書いていますが、半分感想文です。

If they give you ruled paper, write the other way.

JUAN RAMÓN JIMÉNEZ

Fahrenheit 451 by Ray Bradbury.

先日、英語ブログの翻訳を前後編に分けて公開しました。*1

最初に元ブログを流し読みしたときに、これは日本語であまり明文化されていない情報だなと思って翻訳しようと決意しました。結果、多くの人に読んでいただけたようで、とても嬉しいです。

さて、今まで英語論文のまとめという形で翻訳 (どちらかといえば要約) はしてきたものの、ブログのような比較的カジュアルな文章を全文訳すというのはほぼはじめてだったので、今までとは違う場所で時間がかかったりしました。 流し読みする分には問題ないのですが、精訳 (意訳?) するとなると、意外と悩むということに気づきました。 英語の原文と意味に齟齬がないように気をつけながら、流暢な日本語に直すのが大変と感じました。

とはいえ、近年の機械翻訳の技術発展は目覚ましいため、翻訳ツールが我々を支えてくれます。 特に、最近話題の Deepl はすごく便利で、詰まったときはすぐ参考にしました。専門用語を含め、単語の選択もかなり正確だったと思います。

逆に、今回の翻訳のあいだかなり多用したため、欠点もみえてきました。

ひとつは、翻訳結果の省略です。他の人のコメントでちらほら見かけたのですが、なくても問題ない従属節 (特にthough や even if 以降) のフレーズが特に欠落しやすい印象をうけました。

f:id:sh111h:20200706090054p:plain
「and maybe don’t use PyTorch for production code.」が省略されている

この例に関しては踏み込んではいけない領域に踏み込んでしまったかと思って戦慄を覚えています。 Pytorchじゃなくても省略されるか調べてはいけません。

もうひとつはイディオム (慣用句) です。人間としては違和感を感じる部分の訳はだいたいイディオムでした。

具体例をいくつか示します。 "which parts have and have not stood the test of time~" に対して、 「どの部分が時間のテストに耐えているか」 という翻訳結果が出力されました。 「時間のテスト」に違和感があります。

"stand the test of time" について辞書で調べると、時の試練に耐える長く使用される といった意味があり、「時代遅れなのか」と意訳したほうが、しっくりきます。

ほかにも、"not long for this world"「この世界に長くない」 という違和感のある訳になってしまっていましたが、寿命が長くないという意味があるため「 (フレームワークが) 長く使えない」と意訳できます。

もちろん自分の知らないイディオム・表現を正しく翻訳できていた例もあります。

例えば "pain in the neck" は「頸部の痛み」ではなく 「面倒 」"truth be told" は「真実を言えば」と訳さずに「正直に言うと」 と訳していました。

最後に、これは人間でも難しいのですが、uberのアレとか、最近話題のやつみたいな、内輪ネタ・言外の要素を含む アレ の翻訳 (というか補完) はさすがにDeeplでもできません。

"This was what got Uber in so much trouble." から、なんとなくネガティブな想像はできますが、流石に詳細の補完はできないというわけです。

人間でも難しいですし、なにより文化が違うと日本語への翻訳は容易ではないと思われます。 ドメイン知識も必要という点でも、現在進行形の課題といえます。

人間側の工夫

  ここまで、機械翻訳では対応できない例をあげましたが、実際にどう工夫すべきか、個人的な意見を以降に述べます。

今回なによりGoogle-fu (ググるスキル) を高めることが重要だと感じました。 翻訳でいえば、機械翻訳の変な翻訳結果に対して、「ここはイディオムでは?」「もっといい翻訳があるのでは?」と察知して、単語やその関連したフレーズで検索する能力が必要だと思います。

辞書

ググる前に辞書 (サイト) で調べるのも大事だと思います。これは、なるべく正確な情報を先にみたり、検索結果から探す手間を省くという意味で個人的に心がけています。 以下に参考にしている辞書サイトを紹介します。

  • alc の 英辞郎 on the WEB
    • 検索ワードが含まれる単語・フレーズをすべて表示するため、他の辞書サイトと比較して、イディオムの検索に向いている
    • 今回一番活用しました

f:id:sh111h:20200707090626p:plain
「under the hood」の検索結果

  • weblio
    • 例文検索や共起表現検索機能が強い
    • 例文検索は、複数の翻訳文をみて違うニュアンスに書き換えるときに便利
    • 共起表現はどちらかといえばライティングの時に便利で、前置詞 (for, at, in) の選択に迷ったときに使います
      • プラス Grammary で確認すると良い感じにおさまる
    • 一応イディオム辞書もあります https://ejje.weblio.jp/cat/dictionary/eidhg

 

ググる

辞書サイトを活用した上で、最後はググります。

Google検索は曖昧検索ができるので、正しい表現に変換した辞書ページを表示してくれます。

専門用語の場合、wikipedia が検索上位に出てくることが多いかもしれません。このとき、wikipedia (英語) から日本語に切り替えることで日本語訳を確認するというちょっと面倒な使い方ができます。

専門用語の調べ方については、Examine the meaning of unknown terms - Speaker Deck も参考にしてください。

wikipediaは日本語版ページ = 英語版ページの和訳では ない ので、英語版の情報がかなり多いことがあります。 例えば英語版のfacebookページはサービスと企業でベージが分かれており、企業のほうのページにはmottoまで書かれています (https://en.wikipedia.org/wiki/Facebook,_Inc.#History)。 なので、ググると英語ページがヒットします。

また、wikipediaは百科事典ですが、アレゴリー など比喩表現っぽいフレーズも記事として存在しています。 To hell in a handbasket のページには "to hell in a handbasket" 以外のレパートリーが列挙されているので handbasket ではなく、 bucket でググってもヒットしました。

英語圏でよく使われるフレーズであれば、翻訳会社・翻訳家の解説記事、英会話教室のブログなどのページでだいたい意味がみつかります。 今回いちばん「へえ〜」と思ったのは、 high-level は 高いレベルの他に「大まかな」という使い方もある (http://tsubolog.c-brains.jp/14/11/27-100000.php) という知識でした。

uber's trouble のような言外の要素系も、ブログなどでイジっているのであれば、情報としてネット上にある程度存在しています。 今回、その検索した結果のリンクを訳注として追加しています。

イディオムを楽しむ

うんうん悩みながら翻訳して、大変ではあったものの、自分の知らない表現を知ることができたのがよかったです。

流し読みしているときには、よくわからない比喩表現とかイディオムなんかはスルーして読み進めてしまいますが、翻訳するときには、立ち止まる必要があります。

英語圏には英語圏ならではのイディオムがあり、翻訳することで、日本語との対応とセットで理解を深めることができます。

house of cards はトランプカードの家のように不安定という意味ですが、これは日本語のことわざでいう 「砂上の楼閣 (ろうかく)」 に対応しています。 「楼閣」とはカタカナ語でいう「タワー」ですね。 英語だと建物が不安定なのに、日本語だと地盤が不安定なのが面白いなあと思います。

ちなみに、最初に引用した

If they give you ruled paper, write the other way.

ですが、 日本語訳には以下のように注釈がついています。

もし連中が罫紙 (ルールド・ペーパー*) をよこしたら、 逆向きに書きなさい

−−− ファン・ラモン・ヒメネス

*ルールド・ペーパーはふつうの罫紙のことだが、“規則 (ルール) つきの紙” とも訳せる。その場合、この文は「もし連中がルールを押し付けてきたら、反逆しなさい」の意味になる

レイ・ブラッドベリ, 華氏451度. 伊藤 典夫 (訳). 早川書房.

華氏451度〔新訳版〕

華氏451度〔新訳版〕

二つの意味を持っているということですね。 文章の最初にそれっぽい引用をするのが夢だったので引用してみました。

ちなみに「華氏451度」は作品中のセリフにいろんな文芸作品の引用がでてくるので、解説ページならぬ出典ページがついています。

出典がなくてもわかるような知識を持っていると、違う読み方ができるのかなあと思いますが、聖書とかシェイクスピアを誦じれる気は今のところしません。

NLTKを使った英語テキストのtokenize

英語のtokenizeは日本語の分かち書きに比べたら楽なようにみえるが、注意すべき点があるよという紹介をします。

そのために、今回は NLTK (Natural Language Toolkit) を使ってtokenizeします。 (NLTK のVersion 3.5、Python 3.7.4で動作確認しています。)

ちなみにtokenizeは日本語に翻訳すると "トークン化する" ですが、NLTK の関数名と統一するためにtokenizeと表記します。

文のtokenize

複数の文を含む文書を、文単位で分割することを考えます。

文のtokenizeは句点 (period) で区切ればよさそうに見えるが、英語にも「モーニング娘。」のような句点を含む単語があるのでうまくいかない。 簡単なところだと、e.g. さん、 論文テキストだと et al. 先生*1が鬼門です。

# https://www.aclweb.org/anthology/N19-1423/ のアブストラクトより
bert_text = "Unlike recent language representation models (Peters et al., 2018a; Radford et al., 2018), \
BERT is designed to pre-train deep bidirectional representations \
from unlabeled text by jointly conditioning on both left and right context in all layers."

# periodでsplit
bert_text.split('.')
>> 
['Unlike recent language representation models (Peters et al',
 ', 2018a; Radford et al',
 ', 2018), BERT is designed to pre-train deep bidirectional representations from unlabeled text by jointly conditioning on both left and right context in all layers',
 '']

et al. 先生の力によって、1文のはずのテキストが4文になってしまいました。 そういう訳で、tokenizer を使った文を分割する必要性があります。

sent_tokenize

NLTK の場合、文への分割は sent_tokenize を使って行います。 この関数の内部では PunktSentenceTokenizer クラスのpickle を読み込んでいるので、実質PunktSentenceTokenizerでtokenizeしてると考えてよさそうです。

from nltk.data import load
tok = load("tokenizers/punkt/english.pickle")

type(tok)
>> nltk.tokenize.punkt.PunktSentenceTokenizer

tokenizerのメインの挙動は、_slices_from_text の部分。 (github code)

まず、 self._lang_vars.period_context_re() (下記正規表現) を文末の候補の割り出しに使っています。

# self._lang_vars.period_context_re()
\S*                          # some word material
      [\.\?!]             # a potential sentence ending
      (?=(?P<after_tok>
          (?:[?!)";}\]\*:@\'\({\[])              # either other punctuation
          |
          \s+(?P<next_tok>\S+)     # or whitespace and some other token
      ))

この正規表現はちょっと複雑ですが、簡単にいえば、 .?! 以外の他の句読点もしくはスペース+次のtoken( <next_tok>, <after_tok> ) を先読みして判定しています。

その後、 text_contains_sentbreak (github code)でtokenが sentence break かどうか判定して、実際に分割します。

sentence break の判定には 略語 self._params.abbrev_types や コロケーションself._params.collocations のようなさまざまなパラメーターが関わっており、 読むのが面倒 かなり複雑そうなので説明は省きます。

sent_tokenize を使った分割結果は先ほどの例文での et al. 先生にもちゃんと対応しています。

from nltk import sent_tokenize

sent_tokenize(bert_text)
>> ['Unlike recent language representation models (Peters et al., 2018a; Radford et al., 2018), BERT is designed to pre-train deep bidirectional representations from unlabeled text by jointly conditioning on both left and right context in all layers.']

しかしながら、ちょこちょこ対応しきれていない箇所があります。

# 1. 顔文字には対応できない 
sent_tokenize('I love Japan:) I love Tokyo.')
>> ['I love Japan:) I love Tokyo.']

# 2. 対応していない略語もある
sent_tokenize('I love Japan a.k.a. Nippon.')
>> ['I love Japan a.k.a.', 'Nippon.']

# 3. (三点リーダーには対応できるが、)ピリオドの後に空白がないと分割できない
sent_tokenize('I love Japan.I love Tokyo...')
>> ['I love Japan.I love Tokyo...']

# 4. 文末が連続した ! or ? だと最後の1文字で分割されてしまう
sent_tokenize('I love Japan!!! I love Tokyo!!!')
>> ['I love Japan!!!', 'I love Tokyo!!', '!']

1のような比較的noisyなテキストの場合には文単位に区切ろうとせず、 TweetTokenizer (単語のtokenizer) を直接使うとよいと思います。

ちなみに、手元のspacyだと 3, 4は対応していたので、spacy すごいなと思いました (感想)

単語のtokenize

文を単語単位に分割することを考えます。 これまた split() で対応できるのではないか、と思いますが、コロン・コンマ・括弧といった記号は単語と切り離さないと、後々の処理 (tf-idf とか vocab とか) に響きます。

'model (Peters et al., 2018a; Radford et al., 2018), BERT is designed '.split()
>>['model',
 '(Peters',
 'et',
 'al.,',
 '2018a;',
 'Radford',
 'et',
 'al.,',
 '2018),',
 'BERT',
 'is',
 'designed']

上記例でいえば、 '2018),' は2018とは別の語彙として扱われてしまわないよう、 ['2018', ')' ,','] の3つに分けたいです。

word_tokenize

NLTK の単語分割は word_tokenize を使います。

sent_tokenize と同様、内部では NLTKWordTokenizer を呼び出して分割が行われているので、実質NLTKWordTokenizerと同じです。 word_tokenize の場合、 引数 preserve_lineTrue にすれば、 sent_tokenize も同時に実行します。

NLTKWordTokenizer は TreebankTokenizer (後述) を改良したtokenizerです。 ドキュメントにも書いてあるように、このクラスは 正規表現を使った "destructive" (破壊的) なtokenizerなので、detokenizeによる復元が担保されていないので注意が必要です。

このtokenizerで特徴的なのは 'I'll' や 'you're' のような短縮形を [I, 'll], [you, 're] のように分割する点です。

CONTRACTIONS の部分で、'gotta'[got, ta] と分割するルールも設定されています。

# 引用符や省略を含む例文 (すごく適当)
s = '''They're saying that "he is so-- great!". I wanna do this.'''

'_'.join([w for w in word_tokenize(s)])
>> "They_'re_saying_that_``_he_is_so_--_great_!_''_._I_wan_na_do_this_."

また、引用符 "`` に変換されるルール (github code) なので、"destructive" (破壊的) にtokenizeしているといえます。

TreebankWordTokenizer

復元する必要がない場合は、word_tokenize で十分ですが、復元が担保されていない以上、系列ラベリングなど、tokenize 前のテキストの位置情報の必要ある場合に word_tokenize を利用するのは得策ではないと思います。

その場合、別の tokenize module を使いましょう。 span_tokenize 関数をもつクラスであれば、tokenize前に何文字目だったかを取得可能です。

word_tokenize とほぼ同じ実装のTreebankWordTokenizerであれば、 span_tokenize を実行できるので、個人的にこれ一択かなと思います。

元々のTreebankについての説明リンクがことごとく切れているのですが、元々の情報は ftp://ftp.cis.upenn.edu/pub/treebank/public_html/tokenization.html っぽい気がします。

(参考 nlp - Is there an implementation of the Penn Treebank Tokenizer in Perl? - Stack Overflow)

from nltk.tokenize import TreebankWordTokenizer

tree_tok = TreebankWordTokenizer()
for sp in tree_tok.span_tokenize(s):
    print(f'{sp}\t{s[sp[0]:sp[1]]}' )
>>
(0, 4)    They
(4, 7)    're
(8, 14)   saying
(15, 19)  that
(20, 21)  "
(21, 23)  he
(24, 26)  is
(27, 29)  so
(29, 31)  --
(32, 37)  great
(37, 38)  !
(38, 39)  "
(39, 40)  .
(41, 42)  I
(43, 46)  wan
(46, 48)  na
(49, 51)  do
(52, 56)  this
(56, 57)  .

span_tokenize のspan情報を取っておけば、They're の間にスペースが空いていないという情報が保持されるので復元も簡単です。

word_tokenize の時とtokenize結果は大体同じですが、 元のテキストから文字を取得することで、引用符の記号を " のまま使うことができます。

引用符の中の引用符

最後に、ちょっと意地悪なテキストでも正しくtokenizeできるか検証してみます。

Harlan Ellison - Wikipedia のテキストから引用。

Some of his best-known work includes the Star Trek episode "The City on the Edge of Forever", his A Boy and His Dog cycle, and his short stories "I Have No Mouth, and I Must Scream" and "'Repent, Harlequin!' Said the Ticktockman".

タイトルが羅列されていますが、"'Repent, Harlequin!' Said the Ticktockman" ("Repent, Harlequin!" Said the Ticktockman - Wikipedia) がネックになりそう。

タブルクオート" のなかにシングルクオート ' が含まれています。

今まで意識したことはなかったのですが、二重に引用符を使う時は異なる引用符 を使うルールがあるみたいですね。*2

word_tokenize だと _``_'Repent_,_Harlequin_!_'_Said_the_Ticktockman_''_. となってしまい、うまく引用符を分離できませんでした。

elison = '''Some of his best-known work includes the Star Trek episode \ 
"The City on the Edge of Forever", \
his A Boy and His Dog cycle, and his short stories "I Have No Mouth, \
and I Must Scream" and \
"'Repent, Harlequin!' Said the Ticktockman".'''


'_'.join([w for w in word_tokenize(elison)])
>> 
"Some_of_his_best-known_work_includes_the_Star_Trek_episode
_``_The_City_on_the_Edge_of_Forever_''_,
_his_A_Boy_and_His_Dog_cycle_,_and_his_short_stories_``_I_Have_No_Mouth_,
_and_I_Must_Scream_''_and
_``_'Repent_,_Harlequin_!_'_Said_the_Ticktockman_''_."

(※読みやすさのために出力結果に改行を追加してます)

ちなみに spacy だと _"_\'_Repent_,_Harlequin_!_\'_Said_the_Ticktockman_"_.'。ちゃんと分離できました。

import spacy

nlp = spacy.load('en_core_web_sm')

doc = nlp(elison)
'_'.join([d.text for d in doc]) 
>> 
'Some_of_his_best_-_known_work_includes_the_Star_Trek_episode
_"_The_City_on_the_Edge_of_Forever_"_,
_his_A_Boy_and_His_Dog_cycle_,_and_his_short_stories_"_I_Have_No_Mouth_,
_and_I_Must_Scream_"_and
_"_\'_Repent_,_Harlequin_!_\'_Said_the_Ticktockman_"_.'

結論: tokenizerでどれを使えばいいかわからない時は、spacy を使いましょう。

spacy.io

NLTKの資料

ちょっと情報が古いものの、無料で公開されているので困ったらすぐ調べられます。

github.com

入門 自然言語処理

入門 自然言語処理

*1:togetter.com

*2:ハーラン・エリスン - Wikipediaでは 「「悔い改めよ、ハーレクィン!」とチクタクマンはいった」 で普通に同じ括弧を使っているので、日本で「」『』の使い分け方が浸透しているかは不明