Morikatron Engineer Blog

モリカトロン開発者ブログ

英語をカタカナ表記に変換してみる

モリカトロン宮本です。
最近は自然言語処理をゲームに役立てるためのもろもろに取り組んでいます。
さて、今日は英語を無理矢理カタカナ表記に変換する方法をご紹介します。
日本語には「よみ」と「表記」がありますが、機械学習の分野では、「よみ」は比較的軽視されているような印象があります。(英語などの欧州圏の言語処理には必要の無いものだからかも知れません)
また、日本語の処理をする場合には(カタカナ語として定着しているものを除いて)英語は無視される場合が多いようです。
とはいえ、歌詞のように、日本語の文章中に唐突に(時には意味不明な)英語が出てくるような文章を処理する場合、英語を無視できない場合も多いように思います。
そのような、需要が微妙な「英語のカタカナ表記」ですが、意味がわからなくても読み上げたい!という場合に、なかなか適当な方法が見当たらないので作ってみました。

english → イングリシュ
pronouncing → プロナウンシング
といった具合に変換できるようになります。

変換の方法

英語→発音記号→カタカナ
という方法で変換します。

The CMU Pronouncing Dictionary

カーネギーメロン大学が公開している発音辞書です。
http://www.speech.cs.cmu.edu/cgi-bin/cmudict
およそ13万4000語が収録されています。
(発音のバリーションを除くとおよそ12万5000語)
英語→発音記号の変換にはこれを使わせてもらいます。
発音記号にはARPAbetが使われています。
ENGLISH  →  IH NG G L IH SH
 ↑こんな感じに変換されます。

発音記号→カタカナ

発音記号からカタカナへの変換は自作したルールベースで行います。
(くわしくは後述) 

変換処理の作成

データの準備

http://svn.code.sf.net/p/cmusphinx/code/trunk/cmudict/cmudict-0.7b
http://svn.code.sf.net/p/cmusphinx/code/trunk/cmudict/scripts/make_baseform.pl
上記の2つのファイルをダウンロードして、以下のコマンドを実行します。

perl make_baseform.pl cmudict-0.7b cmudict-0.7b_baseform

作成された、 cmudict-0.7b_baseform を使います。

変換処理

発音の分類

発音記号は以下の39種類です。
AA,AE,AH,AO,AW,AY,B,CH,D,DH,EH,ER,EY,F,G,HH,IH,IY,JH,K,L,M,N,NG,OW,OY,P,R,S,SH,T,TH,UH,UW,V,W,Y,Z,ZH
カタカナへの変換の便宜上、これらを「母音」「子音」に分けて考えます。
母音は15種類ありますが、これらを a i u e o の5音に割り振ります。
AE,AW,AY,ER → a
IH,IY → i
UH,UW → u
EH,EY → e
AO,OW,OY → o
AA,AH → 未定

母音のうち AA,AH は曖昧母音です。
どの母音なのか判然としない母音ですが、カタカナ表記するためには a i u e o のいずれかに割り振らないとなりません。
表記を考慮して、 a i e o のいずれかに割り振るようにしてみました。
(発音位置に近い位置にある母音表記を採用する方式。ただし、u は a にする。)

カタカナへの変換

・子音+母音の場合
母音と子音の組み合わせをカタカナに変換します。

#カタカナへの変換用辞書
{
    'B':{'a':'バ','i':'ビ','u':'ブ','e':'ベ','o':'ボ','':'ブ'},
    'CH':{'a':'チャ','i':'チ','u':'チュ','e':'チェ','o':'チョ','':'チ'},
    'D':{'a':'ダ','i':'ディ','u':'ドゥ','e':'デ','o':'ド','':'ド'},
    'DH':{'a':'ザ','i':'ジ','u':'ズ','e':'ゼ','o':'ゾ','':'ズ'},
    'F':{'a':'ファ','i':'フィ','u':'フ','e':'フェ','o':'フォ','':'フ'},
    'G':{'a':'ガ','i':'ギ','u':'グ','e':'ゲ','o':'ゴ','':'グ'},
    'HH':{'a':'ハ','i':'ヒ','u':'フ','e':'ヘ','o':'ホ','':'フ'},
    'JH':{'a':'ジャ','i':'ジ','u':'ジュ','e':'ジェ','o':'ジョ','':'ジ'},
    'K':{'a':'カ','i':'キ','u':'ク','e':'ケ','o':'コ','':'ク'},
    'L':{'a':'ラ','i':'リ','u':'ル','e':'レ','o':'ロ','':'ル'},
    'M':{'a':'マ','i':'ミ','u':'ム','e':'メ','o':'モ','':'ム'},
    'N':{'a':'ナ','i':'ニ','u':'ヌ','e':'ネ','o':'ノ','':'ン'},
    'NG':{'a':'ンガ','i':'ンギ','u':'ング','e':'ンゲ','o':'ンゴ','':'ング'},
    'P':{'a':'パ','i':'ピ','u':'プ','e':'ペ','o':'ポ','':'プ'},
    'R':{'a':'ラ','i':'リ','u':'ル','e':'レ','o':'ロ','':'ー'},
    'S':{'a':'サ','i':'シ','u':'ス','e':'セ','o':'ソ','':'ス'},
    'SH':{'a':'シャ','i':'シ','u':'シュ','e':'シェ','o':'ショ','':'シュ'},
    'T':{'a':'タ','i':'ティ','u':'チュ','e':'テ','o':'ト','':'ト'},
    'TH':{'a':'サ','i':'シ','u':'シュ','e':'セ','o':'ソ','':'ス'},
    'V':{'a':'バ','i':'ビ','u':'ブ','e':'ベ','o':'ボ','':'ブ'},
    'W':{'a':'ワ','i':'ウィ','u':'ウ','e':'ウェ','o':'ウォ','':'ウ'},
    'Y':{'a':'ア','i':'','u':'ュ','e':'エ','o':'ョ','':'イ'},
    'BOS_Y':{'a':'ヤ','i':'イ','u':'ユ','e':'イエ','o':'ヨ','':'イ'},
    'Z':{'a':'ザ','i':'ジ','u':'ズ','e':'ゼ','o':'ゾ','':'ズ'},
    'ZH':{'a':'ジャ','i':'ジ','u':'ジュ','e':'ジェ','o':'ジョ','':'ジュ'},
    'T_S':{'a':'ツァ','i':'ツィ','u':'ツ','e':'ツェ','o':'ツォ','':'ツ'},
}


・子音が連続する場合
子音単独でカタカナに変換します。
例) T → ト
前述の変換用辞書の各子音の末尾にこれらのデータを追加しました。

特殊なルール

・子音を含む母音は後続が子音の場合と母音の場合で変化する。
 ・Y音を含む母音(AY EY OY)
 ・W音を含む母音(AY EY OY)
 ・R音を含む母音(ER)
・T S の組み合わせ(1個の子音扱いに)
・NG + K または G の組み合わせ(G音の削除)
・D Z の組み合わせ(D音の削除)
・Y の処理(拗音またはヤ行に変換)
・R の処理
この辺のルールは変換結果を見ながら調整していきます。

完成したコード

https://github.com/morikatron/snippet/blob/master/english_to_kana/english_to_kana.py

# -*- coding: utf-8 -*-
"""
Morikatron Engineer Blog の記事 「英語をカタカナ表記に変換してみる」のサンプルコードです。詳しくは下記URLのブログ記事をご参照ください。
https://tech.morikatron.ai/entry/2020/05/25/100000

プログラムの実行にあたっては
http://svn.code.sf.net/p/cmusphinx/code/trunk/cmudict/cmudict-0.7b
http://svn.code.sf.net/p/cmusphinx/code/trunk/cmudict/scripts/make_baseform.pl
上記の2つのファイルを本プログラムと同一ディレクトリにダウンロードして、
以下のコマンドを実行してください。
perl make_baseform.pl cmudict-0.7b cmudict-0.7b_baseform
これにより作成されるファイル cmudict-0.7b_baseform を本プログラムで読み込んで利用します。

本プログラムは python 3.4 以降で実行してください。
"""

import pathlib


class EnglishToKana:

    def __init__(self, log=False):

        global log_text
        self.vowels = {
            'AA': '',  # 曖昧
            'AH': '',  # 曖昧
            'AE': 'a',
            'AO': 'o',
            'AW': 'a',
            'AY': 'a',
            'EH': 'e',
            'ER': 'a',
            'EY': 'e',
            'IH': 'i',
            'IY': 'i',
            'OW': 'o',
            'OY': 'o',
            'UH': 'u',
            'UW': 'u',
        }

        self.kana_dic = {
            'B': {'a': 'バ', 'i': 'ビ', 'u': 'ブ', 'e': 'ベ', 'o': 'ボ', '': 'ブ'},  # be	B IY
            'CH': {'a': 'チャ', 'i': 'チ', 'u': 'チュ', 'e': 'チェ', 'o': 'チョ', '': 'チ'},  # cheese	CH IY Z#チch
            'D': {'a': 'ダ', 'i': 'ディ', 'u': 'ドゥ', 'e': 'デ', 'o': 'ド', '': 'ド'},  # dee	D IY
            'DH': {'a': 'ザ', 'i': 'ジ', 'u': 'ズ', 'e': 'ゼ', 'o': 'ゾ', '': 'ズ'},  # thee	DH IY
            'F': {'a': 'ファ', 'i': 'フィ', 'u': 'フ', 'e': 'フェ', 'o': 'フォ', '': 'フ'},  # fee	F IY
            'G': {'a': 'ガ', 'i': 'ギ', 'u': 'グ', 'e': 'ゲ', 'o': 'ゴ', '': 'グ'},  # green	G R IY N
            'HH': {'a': 'ハ', 'i': 'ヒ', 'u': 'フ', 'e': 'ヘ', 'o': 'ホ', '': 'フ'},  # he	HH IY#H
            'JH': {'a': 'ジャ', 'i': 'ジ', 'u': 'ジュ', 'e': 'ジェ', 'o': 'ジョ', '': 'ジ'},  # gee	JH IY#J
            'K': {'a': 'カ', 'i': 'キ', 'u': 'ク', 'e': 'ケ', 'o': 'コ', '': 'ク'},  # key	K IY
            'L': {'a': 'ラ', 'i': 'リ', 'u': 'ル', 'e': 'レ', 'o': 'ロ', '': 'ル'},  # lee	L IY
            'M': {'a': 'マ', 'i': 'ミ', 'u': 'ム', 'e': 'メ', 'o': 'モ', '': 'ム'},  # me	M IY
            'N': {'a': 'ナ', 'i': 'ニ', 'u': 'ヌ', 'e': 'ネ', 'o': 'ノ', '': 'ン'},  # knee	N IY
            'NG': {'a': 'ンガ', 'i': 'ンギ', 'u': 'ング', 'e': 'ンゲ', 'o': 'ンゴ', '': 'ング'},  # ping	P IH NG
            'P': {'a': 'パ', 'i': 'ピ', 'u': 'プ', 'e': 'ペ', 'o': 'ポ', '': 'プ'},  # pee	P IY
            'R': {'a': 'ラ', 'i': 'リ', 'u': 'ル', 'e': 'レ', 'o': 'ロ', '': 'ー'},  # read	R IY D
            'S': {'a': 'サ', 'i': 'シ', 'u': 'ス', 'e': 'セ', 'o': 'ソ', '': 'ス'},  # sea	S IY
            'SH': {'a': 'シャ', 'i': 'シ', 'u': 'シュ', 'e': 'シェ', 'o': 'ショ', '': 'シュ'},  # she	SH IY
            'T': {'a': 'タ', 'i': 'ティ', 'u': 'チュ', 'e': 'テ', 'o': 'ト', '': 'ト'},  # tea	T IY
            'TH': {'a': 'サ', 'i': 'シ', 'u': 'シュ', 'e': 'セ', 'o': 'ソ', '': 'ス'},  # theta	TH EY T AH
            'V': {'a': 'バ', 'i': 'ビ', 'u': 'ブ', 'e': 'ベ', 'o': 'ボ', '': 'ブ'},  # vee	V IY
            'W': {'a': 'ワ', 'i': 'ウィ', 'u': 'ウ', 'e': 'ウェ', 'o': 'ウォ', '': 'ウ'},  # we	W IY
            'Y': {'a': 'ア', 'i': '', 'u': 'ュ', 'e': 'エ', 'o': 'ョ', '': 'イ'},  # yield	Y IY L D
            'BOS_Y': {'a': 'ヤ', 'i': 'イ', 'u': 'ユ', 'e': 'イエ', 'o': 'ヨ', '': 'イ'},
            'Z': {'a': 'ザ', 'i': 'ジ', 'u': 'ズ', 'e': 'ゼ', 'o': 'ゾ', '': 'ズ'},  # zee	Z IY
            'ZH': {'a': 'ジャ', 'i': 'ジ', 'u': 'ジュ', 'e': 'ジェ', 'o': 'ジョ', '': 'ジュ'},  # seizure	S IY ZH ER
            'T_S': {'a': 'ツァ', 'i': 'ツィ', 'u': 'ツ', 'e': 'ツェ', 'o': 'ツォ', '': 'ツ'},
        }

        if log:
            log_text = ''

        # 変換用辞書
        self.eng_kana_dic = {}

        # CMU辞書読み込み
        path_to_cmu = pathlib.Path(__file__).parent / './cmudict-0.7b_baseform'
        with open(path_to_cmu, 'r', encoding='us-ascii', errors='ignore') as f:
            lines = f.read().split('\n')
            for line in lines:
                if line == '':
                    continue
                word, p = line.split('\t')

                if not (0x41 <= ord(word[0]) <= 0x5a):
                    # アルファベット以外(記号とか)から始まる単語は無視
                    continue
                if '(' in word:
                    # '('を含む単語も無視 発音のバリエーションだから
                    continue
                word = word.lower()  # 小文字にしておく

                sound_list = p.split(' ')
                yomi = ''

                # EOS と BOS をつけておく
                sound_list = ['BOS'] + sound_list + ['EOS']
                for i in range(1, len(sound_list) - 1):

                    s = sound_list[i]
                    s_prev = sound_list[i - 1]
                    s_next = sound_list[i + 1]

                    if s_prev == 'BOS' and s == 'Y':
                        # 頭がYの場合特殊
                        s = sound_list[i] = 'BOS_Y'

                    if s in self.kana_dic and s_next not in self.vowels:
                        # 子音(→子音)
                        if s_next in {'Y'}:
                            # 後ろが Y の場合イ行に
                            # ただし2文字の場合は2文字目を削る 例)フィ→フ
                            yomi += self.kana_dic[s]['i'][0]
                        elif s == 'D' and s_next == 'Z':
                            # D音をスキップ
                            continue
                        elif s == 'T' and s_next == 'S':
                            # 連結して'T_S'に
                            sound_list[i + 1] = 'T_S'
                            continue
                        elif s == 'NG' and s_next in {'K', 'G'}:
                            # 'NG'の次が 'G' or 'K' の場合2文字目を削る 例)ング→ン
                            yomi += self.kana_dic[s][''][0]
                        elif s_prev in {'EH', 'EY', 'IH', 'IY'} and s == 'R':
                            yomi += 'アー'
                        else:
                            yomi += self.kana_dic[s]['']
                    elif s in self.vowels:
                        # 母音
                        # aiueoに割り振る
                        if s in {'AA', 'AH'}:
                            # 曖昧母音
                            v = self.find_vowel(word, i - 1, len(sound_list) - 2)
                        else:
                            v = self.vowels[s]

                        if s_prev in self.kana_dic:
                            # (子音→)母音で
                            # print(s,v)
                            yomi += self.kana_dic[s_prev][v]
                        else:
                            # (母音→)母音
                            # 母音が連続すると変化するもの
                            if s_prev in {'AY', 'EY', 'OY'} and s not in {'AA', 'AH'}:  # 曖昧母音の場合は除外
                                yomi += {'a': 'ヤ', 'i': 'イ', 'u': 'ユ', 'e': 'エ', 'o': 'ヨ'}[v]
                            elif s_prev in {'AW', 'UW'}:
                                yomi += {'a': 'ワ', 'i': 'ウィ', 'u': 'ウ', 'e': 'ウェ', 'o': 'ウォ'}[v]
                            elif s_prev in {'ER'}:
                                yomi += {'a': 'ラ', 'i': 'リ', 'u': 'ル', 'e': 'レ', 'o': 'ロ'}[v]
                            else:
                                # 変化しない
                                yomi += {'a': 'ア', 'i': 'イ', 'u': 'ウ', 'e': 'エ', 'o': 'オ'}[v]

                        # Yを母音化
                        if s in {'AY', 'EY', 'OY'}:  # これは常に入れてOK?
                            yomi += 'イ'
                        # 後続が母音かどうかで変化するもの
                        if s_next not in self.vowels:
                            # 母音(→子音)
                            if s in {'ER', 'IY', 'OW', 'UW'}:
                                yomi += 'ー'
                            elif s in {'AW'}:
                                yomi += 'ウ'

                if log:
                    log_text += word + ' ' + yomi + ' ' + p + '\n'
                # 登録
                self.eng_kana_dic[word] = yomi

        if log:
            with open('log.txt', 'w') as f_out:
                f_out.write(log_text)

    # 表記から母音を取り出す関数(曖昧母音用)
    def find_vowel(self, text, pos, length):
        p = (pos + 0.5) / length
        lengthoftext = len(text)
        distance_list = []
        vowel_list = []
        for i, s in enumerate(text):  # type: (int, object)
            if s in {'a', 'i', 'u', 'e', 'o'}:
                vowel_list.append(s)
                distance_list.append(abs(p - (i + 0.5) / lengthoftext))
        if len(distance_list) == 0:
            # 母音が無い
            return 'a'
        v = vowel_list[distance_list.index(min(distance_list))]
        # uはaに置き換える
        if v == 'u':
            v = 'a'
        return v

    def convert(self, english):
        english = english.lower()
        if english in self.eng_kana_dic:
            return self.eng_kana_dic[english]
        else:
            return 'ERROR 辞書にありません'


if __name__ == "__main__":
    e2k = EnglishToKana()
    print(e2k.convert('english'))


以下のように実行することで、英語をカタカナに変換することができます。

>>> from english_to_kana import EnglishToKana
>>> e2k = EnglishToKana()
>>> print(e2k.convert('english'))
イングリシュ

変換例

こんな感じに変換されます。
(長い単語をピックアップしてみました)
arteriosclerosis (AA R T IH R IY OW S K L ER OW S AH S)
 → アーティリオースクラローシス
biotechnological (B AY OW T EH K N AH L AA JH IH K AH L)
 → バイヨーテクノロジカル
chlorofluorocarbons (K L AO R OW F L AO R OW K AA R B AA N Z)
 → クロローフロローカーボンズ
deinstitutionalization (D IY IH N S T IH T UW SH AH N AH L AH Z EY SH AH N)
 → ディインスティチューショナリゼイション
ethnomusicologist (EH TH N AH M Y UW Z AH K AA L AH JH IH S T)
 → エスノミュージコロジスト
french-polynesia (F R EH N CH P AA L IH N IY ZH AH)
 → フレンチポリニージャ
genossenschaftsbank (G EH N OW S EH N SH AE F T S B AE NG K)
 → ゲノーセンシャフツバンク
immunoperoxidase (IH M Y UW N OW P EH R AO K S IH D EY Z)
 → イミューノーペロクシデイズ
mischaracterizations (M IH S K AE R AH K T ER AH Z EY SH AH N Z)
 → ミスカラクタリゼイションズ
psychotherapeutic (S AY K OW TH EH R AH P Y UW T IH K)
 → サイコーセレピューティク
thousand-years-long (TH AW Z AH N D Y IH R Z L AO NG)
 → サウザンデアーズロング
zentralsparkasse (Z EH N T R AH L S P AA R K AA S IH)
 → ゼントラルスパーカシ

意味のわからない単語ばかりですが、まあまあ良さそうな感じですね。

ちなみに、CMU Pronouncing Dictionary には、日本人の名字もいくつか収録されていました。

miyamoto (M IY Y AA M OW T OW)
 → ミーアモートー

……。
たしかに、そんな感じに呼ばれていたような気もします。

さらに改善するなら

まあまあいい感じになりましたが、さらに改善する場合は以下の部分を改善すると良さそうです。

■変換ルールをさらに追加する
まだまだ不自然なカタカナ表記にあるものがありますが、さらにルールを加えていくと良くなりそうです。
例えば、
・「ッ」を追加するルール。
 book→ B UH K →「ブク」
 となるところを「ブック」となるように。
・複合語などが連結しないようにする。
 never-ending → 「ネバレンディング」
 となるところを「ネバーエンディング」となるように。
 
■曖昧母音の処理にさらに細かいルールを作る。
表記を頼りに単純なルールで母音を決定していますが、さらに細かいルールを作ると改善しそうです。

■外来語として定着している単語は、別の方法で読みを取得する。
発音記号からカタカナ表記を作っていますので、towel(タオル)が「タウェル」に変換されます。
外来語として定着しているものは英語以外の起源のものも多く、普通に使われている表記が必要であれば、読みが取得できる形態素解析ツールなどで読みを取得すると良さそうです。