spaCyで無能翻訳機を作る【自然言語処理, Python】
目次
spaCyで無能翻訳機を作る
私たちは言葉を使ってコミュニケーションを取りますが、この言葉のことを「自然言語」と言います。
パソコンで機械的にこの自然言語を解析することを「自然言語処理」と言います。
Pythonには自然言語処理を行えるライブラリにspaCyというライブラリがあります。
今回はこのspaCyを使って「無能翻訳機」を作ってみたいと思います。
具体的には↓を見ていきます。
無能翻訳機とは?
スクリプトの実行結果
スクリプトの設計
スクリプトのソースコード
ソースコードの解説
無能翻訳機とは?
混沌なる自然言語処理の世界に突如舞い降りた機械翻訳機、その名も無能翻訳機とはどういうものでしょうか。
無能翻訳機は、仕事をしない翻訳機です。基本的に無能で、役に立ちません。
たまに仕事をしてるようなそぶりを見せますが、気が付くとデタラメなことを言っているそんな翻訳機です。
この翻訳機は筆者が機械翻訳について調べている過程で生まれました。
機械翻訳を実行できるスクリプトを何はともあれ作ってみようと考えた筆者は、あえなく機械翻訳に撃沈しました。
むずかしい。機械翻訳むずかしい。うぐぐ、わからん。
(^ _ ^) | 機械翻訳わからん |
しかし私もただで倒れるわけには行きません。
コアとなる翻訳部分をいっさいかなぐり捨てて、この世に無能翻訳機を生み出しました。
この翻訳機はなんと字句解析さえ出来れば翻訳しているように振る舞うすぐれもので、一部テストケースも通過します。
私は機械翻訳の前に倒れましたが、手負いの幼子無能翻訳機をこの世に残すことができました。悔いはありません。
ああ、栄光の自然言語処理。恒久なる機械翻訳。我に手を差し伸べたまへ。
スクリプトの実行結果
今回のスクリプトは単体テストで動作検証を行っています。
後述のソースコードをsample.py
などに保存して端末からpython -m unittest sample
を実行すると、テストケースを実行できます。
実行すると↓のような結果になります。
. ---------------------------------------------------------------------- Ran 1 test in 0.348s OK
スクリプトの設計
スクリプトのコアな実装はTranslator
というクラスに実装します。
このクラスのtrans_jpn()
メソッドが翻訳のエントリーポイントです。
Translator
は内部に辞書を持っています。
翻訳時に単語をこの辞書に照合して翻訳後のワードを出力します。
trans_jpn()
では入力のテキストをspaCyで解析し、ドキュメントにします。
このドキュメントから文(sent
)を1文ずつ取り出して解析します。
文の解析ではトークンを1つずつ取り出して、このトークンのtext
属性を参照します。
text
から翻訳後の単語を導き出してそれを結果のテキストに保存します。
そうしておいて文の解析が終わったらそのテキストを結果として返します。
基本的な設計は↑のようになります。
依存構造などは利用しておらず、字句解析の結果のみを使った解析になっています。
そのためspaCyなどを使わなくてもMeCabやJanomeなどでも代用できるかと思います。
スクリプトのソースコード
↓がソースコード全文です。
""" 「無能」な機械翻訳を行う License: MIT Created at: 2021/02/26 """ import spacy import unittest # GiNZAのモデルをロード nlp = spacy.load('ja_ginza') class Translator: """ 無能な機械翻訳を行う翻訳機 """ def __init__(self): # 辞書 self.jpn_to_eng = { 'これ': 'this', 'は': 'is', 'ペン': 'pen', '私': 'me', 'の': 'of', '。': '.', } def trans_jpn(self, text): """ 引数textを解析して翻訳を生成する """ doc = nlp(text) # spaCyで解析する results = [] for sent in doc.sents: # 文を取り出す result = self.trans_jpn_sent(sent) # 文ごとに解析する if result: results.append(result) return ' '.join(results) def trans_jpn_sent(self, sent): """ 引数sentを解析する """ s = '' for tok in sent: if tok.pos_ == 'NOUN': s += 'a ' # 名詞だったらaを付けておく if tok.text == '。': s = s.rstrip() translated = self.jpn_to_eng[tok.text] # 単語を翻訳 s += translated + ' ' s = s.rstrip() return s class Test(unittest.TestCase): def eq(self, a, b): t = Translator() c = t.trans_jpn(a) self.assertEqual(c, b) def test_analyze(self): # うまく行ってるように見えるケース self.eq('これはペン。', 'this is a pen.') self.eq('ペンはこれ。', 'a pen is this.') self.eq('これはペン。ペンはこれ。', 'this is a pen. a pen is this.') # うまく行かないケース self.eq('これは私のペン。', 'this is me of a pen.') self.eq('私のペンはこれ。', 'me of a pen is this.')
ソースコードの解説
ここからソースコードの解説になります。
必要モジュールのインポート
必要モジュールを最初の方でインポートします。
今回は自然言語処理にspaCyを使いますのでspacy
をインポートします。
それから単体テスト用にunittest
もインポートします。
import spacy import unittest
GiNZAのロード
spaCyからGiNZAのモデルをロードします。
これは日本語の解析で必要になります。
↓のようにspacy.load()
にja_ginza
を渡してロードすると結果がginza.Japanese
で返ってきます。
これに慣例的にnlp
と名付けておきます。
# GiNZAのモデルをロード nlp = spacy.load('ja_ginza')
Translatorの作成
Translator
クラスを作ります。
jpn_to_eng
という属性、これは辞書ですが、この辞書に日本語と英語の対応を入れておきます。
翻訳時にこの辞書を参照して無能翻訳機は日本語を英語に翻訳します。
ご覧の通り、対応している単語は非常に少ないです。
class Translator: """ 無能な機械翻訳を行う翻訳機 """ def __init__(self): # 辞書 self.jpn_to_eng = { 'これ': 'this', 'は': 'is', 'ペン': 'pen', '私': 'me', 'の': 'of', '。': '.', } ...
trans_jpn()で日本語を英語に翻訳
trans_jpn()
は引数text
をspaCyで解析して翻訳文を生成します。
nlp()
にテキストを私、ドキュメントを生成しています。これはspacy.tokens.doc.Doc
です。
このドキュメントのsents
属性をfor
文で回すと、文を1文ずつ取り出せます。
取り出した文(sent
)をtrans_jpn_sent()
に渡して解析を分岐します。
結果はresults
というリストに保存し、最後の方で半角スペースで結合して文字列にします。
def trans_jpn(self, text): """ 引数textを解析して翻訳を生成する """ doc = nlp(text) # spaCyで解析する results = [] for sent in doc.sents: # 文を取り出す result = self.trans_jpn_sent(sent) # 文ごとに解析する if result: results.append(result) return ' '.join(results)
trans_jpn_sent()で文を翻訳する
trans_jpn_sent()
は引数sent
を解析して翻訳文を生成します。
sent
はspacy.tokens.span.Span
ですが、これはfor
文で回せます。
sent
をfor
文で回すとトークン(spacy.tokens.token.Token
)を取り出せるので、このトークンのpos_
属性を参照します。
pos_
属性は品詞を表す属性で、これがNOUN
だった場合、そのトークンは名詞になります。
トークンが名詞だったら「a
」を出力に追加しています。これはthis is a pen
のa
の部分になります。
トークンのtext
属性には元の文のそのままの表記が保存されていますが、これが「。」だったら文字列の右側の半角スペースを除去します。
これはピリオドを単語にくっつけるためです。
jpn_to_eng
をトークンのtext
属性で参照して、その単語の翻訳(translated
)を手に入れます。
これを結果に加えて、ループが完了したら末尾の半角スペースを除去して返します。
def trans_jpn_sent(self, sent): """ 引数sentを解析する """ s = '' for tok in sent: if tok.pos_ == 'NOUN': s += 'a ' # 名詞だったらaを付けておく if tok.text == '。': s = s.rstrip() translated = self.jpn_to_eng[tok.text] # 単語を翻訳 s += translated + ' ' s = s.rstrip() return s
テストを書く
テストを書きます。
unittest.TestCase
を継承したらクラスを作り、test_
で始まるメソッドを定義します。
このメソッドがテストで実行されます。
テスト内容を見ると「これはペン」や「ペンはこれ」などの日本語は正しく翻訳できているように見えます。
これはこの文の場合の構造が似ているためです。
構造が違う場合、つまり「これは私のペン」などはまったくでたらめな翻訳文を生成します。
これは無能翻訳機が文の構造の解析を一切やっていないためです。
(^ _ ^) | まさに無能 |
class Test(unittest.TestCase): def eq(self, a, b): t = Translator() c = t.trans_jpn(a) self.assertEqual(c, b) def test_analyze(self): # うまく行ってるように見えるケース self.eq('これはペン。', 'this is a pen.') self.eq('ペンはこれ。', 'a pen is this.') self.eq('これはペン。ペンはこれ。', 'this is a pen. a pen is this.') # うまく行かないケース self.eq('これは私のペン。', 'this is me of a pen.') self.eq('私のペンはこれ。', 'me of a pen is this.')
おわりに
今回はspaCyで無能翻訳機を作ってみました。
果たしてその価値は不明ですが、自然言語処理に慣れるための遊びには向いているかもしれないと思いました。
(^ _ ^) | いつかリベンジしようね |