翻訳モデルの翻訳時の語順の並び替えを可視化してみた

# できたもの

https://huggingface.co/spaces/sentencebird/translation-word-order

入力を英語の**"I saw a girl with a telescope in the garden"**という文にして、いろんな言語で翻訳を試してみます。

- 日本語
![スクリーンショット 2021-11-15 20.29.13.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/638747/b2c5fb72-c67e-1003-0c4a-df5276216aef.png)

- 中国語
![スクリーンショット 2021-11-22 20.12.23.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/638747/f4e18da2-3e71-6753-8854-d510d837409d.png)

- 韓国語
![スクリーンショット 2021-11-22 20.12.32.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/638747/4c311ced-678e-ae36-4fe0-d787b4124988.png)

- ドイツ語
![スクリーンショット 2021-11-22 20.12.39.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/638747/d887fb71-af44-bce7-916c-07c4d0aef273.png)

- スペイン語
![スクリーンショット 2021-11-22 20.12.44.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/638747/522af39e-47e1-7107-a9cc-e0518e3a4874.png)

- ロシア語
![スクリーンショット 2021-11-22 20.12.50.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/638747/d15818a2-b958-2883-098d-c1c8644b06e0.png)

日本語から別の言語に翻訳することもできます。

- 英語
![スクリーンショット 2021-11-22 20.12.57.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/638747/152385ec-b77d-cee7-b9f4-ad36d1a8ada6.png)

- 韓国語
![スクリーンショット 2021-11-22 20.13.07.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/638747/fc1ca4d3-10f4-1b7c-6823-51429e91696b.png)

# 実装
- python 3.6.5

使用したライブラリは以下の通りです。

- transformers （翻訳）
- pyvis （単語の対応のネットワーク図作成）
- streamlit （入出力をwebアプリ化）

## 翻訳時の語の並び替えとアテンション

翻訳元と翻訳先の語の対応を調べるために、翻訳モデル（Transformer）のアテンションの重みを使用しています。

https://github.com/jessevig/bertviz

このbertvizというモジュールを用いると、Transformerのアテンションの重みを可視化することができます。
bertvizでアテンションを可視化した以下の図を見てみると、翻訳先の語から見ると、その語に対応する翻訳元の語に大きな重みがつきやすいようです。

![スクリーンショット 2021-11-22 12.55.36.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/638747/4b5fcb44-254b-053a-085d-6d0053ec4b9b.png)

今回はこの重みの一番大きな部分を翻訳間で対応した語とみなして、可視化するものを作りました。

## 翻訳モデル

今回はhuggingfaceの多言語翻訳モデルのmbart-large-50-many-to-many-mmtを使用しました。

https://huggingface.co/facebook/mbart-large-50-many-to-many-mmt

> The model can translate directly between any pair of 50 languages. 

とのことで、50ヶ国語を翻訳元、そして翻訳先の言語に指定することができます。

基本的な使い方として、以下のようにtokenizerに翻訳元言語の`src_lang`と翻訳先の言語`lang_code_to_id`を指定することで、翻訳結果を出力することができます。

```python
from transformers import MBartForConditionalGeneration, MBart50TokenizerFast

article_hi = "संयुक्त राष्ट्र के प्रमुख का कहना है कि सीरिया में कोई सैन्य समाधान नहीं है"
article_ar = "الأمين العام للأمم المتحدة يقول إنه لا يوجد حل عسكري في سوريا."

model = MBartForConditionalGeneration.from_pretrained("facebook/mbart-large-50-many-to-many-mmt")
tokenizer = MBart50TokenizerFast.from_pretrained("facebook/mbart-large-50-many-to-many-mmt")

# 英日翻訳
src_text = 'I saw a girl with a telescope in the garden.'

tokenizer.src_lang = "en_JA"
encoded_en = tokenizer(src_text, return_tensors="pt")
generated_tokens = model.generate(**encoded_en, forced_bos_token_id=tokenizer.lang_code_to_id["ja_XX"])
tokenizer.batch_decode(generated_tokens, skip_special_tokens=True)
#=> ['私は庭で望遠鏡を持つ女の子を見た。']
```

modelとtokenizerをロードする関数を定義した後、`Translation`モデルの中で翻訳元と翻訳前のトークンごとのアテンションの対応から、語順の変化を推測しています。

```python
def load_model():
    return MBartForConditionalGeneration.from_pretrained("facebook/mbart-large-50-many-to-many-mmt")

def load_tokenizer():
    return MBart50TokenizerFast.from_pretrained("facebook/mbart-large-50-many-to-many-mmt", use_fast=False)

class Translation():
    def __init__(self, src_lang, dest_lang):
        self.model = load_model()
        self.tokenizer = load_tokenizer()
        self.tokenizer.src_lang = src_lang
        self.dest_lang = dest_lang

    def process(self, src_text):
        encoded = self.tokenizer(src_text, return_tensors="pt")
        generated_tokens = self.model.generate(**encoded, forced_bos_token_id=self.tokenizer.lang_code_to_id[self.dest_lang])
        generated_texts = self.tokenizer.batch_decode(generated_tokens, skip_special_tokens=True)
        self.dest_text = generated_texts[0]

        encoder_input_ids = self.tokenizer(src_text, return_tensors="pt").input_ids
        decoder_input_ids = self.tokenizer(self.dest_text, return_tensors="pt").input_ids

        self.outputs = self.model(input_ids=encoder_input_ids, decoder_input_ids=decoder_input_ids, output_attentions=True)

        self.encoder_text = self.tokenizer.convert_ids_to_tokens(encoder_input_ids[0])
        self.decoder_text = self.tokenizer.convert_ids_to_tokens(decoder_input_ids[0])

        # 最終層のアテンションの平均を取る
        mean_cross_attentions = tuple([torch.mean(self.outputs.cross_attentions[0], 1, True)])

        # アテンションの中で一番大きな値を持っているトークンの組み合わせのみを抽出して、それを翻訳元と翻訳先の並び替えとする
        self.positions = [int(i) for i in torch.argmax(mean_cross_attentions[0], dim=2).flatten()]
```

このモデルを使って、翻訳結果や翻訳時のアテンションから推測した語順の対応を取得します。

```python
translation = Translation("en_JA")
translation.process("I saw a girl with a telescope.")

print(translation.encoder_text)
#=> ['<unk>', '▁I', '▁saw', '▁a', '▁girl', '▁with', '▁a', '▁tele', 'scope', '.', '
']

print(translation.decoder_text)
#=> ['<unk>', '▁私は', '、', '望', '遠', '鏡', 'を持つ', '女の子', 'を見た', '。', '
']

print(translation.positions)
#=> [ 0,  1,  9,  8,  5,  6,  8,  4,  2,  3, 11, 11]
```

## 可視化
トークンをノード、翻訳元と翻訳先のトークンの対応をエッジとしてネットワーク図として描画します。

```python
from pyvis.network import Network

class TranslationNetwork():
    def __init__(self, network):
        self.network = network
        self.n_nodes = 0
        self.n_src_nodes = 0
        self.n_dest_nodes = 0
        
    def add_nodes(self, words, group):
        if group == "src":
            self.n_src_nodes = len(words)
            group_i = 0
            hidden_nodes_i = [0, self.n_src_nodes-1]
        elif group == "dest":
            self.n_dest_nodes = len(words)
            group_i = 1
            hidden_nodes_i = [0, self.n_dest_nodes-1]

        # <unk>タグや
タグを除いています
        self.hidden_edges_i = [0, self.n_src_nodes-1, self.n_src_nodes, self.n_src_nodes+self.n_dest_nodes-1]

        size = 10
        x_margin, y_margin = 100, 100        
        for i, word in enumerate(words):
            hidden = i in hidden_nodes_i
            self.network.add_node(self.n_nodes, shape="square", label=word, group=f"{group}", x=i*x_margin, y=group_i*y_margin, size=size, physics=False, hidden=hidden)
            self.n_nodes += 1

    def add_edges(self, positions):
        for i, position in enumerate(positions):
            j = self.n_src_nodes + position
            hidden = i in self.hidden_edges_i or j in self.hidden_edges_i
            self.network.add_edge(i, j, color="gray", hidden=hidden)        
```

`Translation`モデルで得た結果を使いながら、次のようにネットワークを構成していくと、

```python
tn = TranslationNetwork(Network(width="100%", height="300px"))
tn.add_nodes(translation.encoder_text, group="src") # translation.encoder_textは翻訳元の単語のリスト
tn.add_nodes(translation.decoder_text, group="dest")  # translation.encoder_textは翻訳結果の単語のリスト
tn.add_edges(translation.positions) # 語順の対応

tn.network.show("translation.html") # ファイルに出力
```

このような表示のHTMLが出力されます。

![スクリーンショット 2021-11-22 19.48.16.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/638747/88b0da8c-cb9d-ecac-9523-de7d3226984c.png)



## 入力フォーム作成
streamlitを利用してテキストの入力から結果の出力までをインタフェース化します。

最終的に以下のコマンドでwebブラウザが立ち上がり、インタフェースが起動します。

```bash
streamlit run https://raw.githubusercontent.com/sentencebird/translation-word-order/main/app.py
```

[#できたもの](#できたもの)にあるようなページが表示されます。

#おわりに
今回は語順の対応の可視化を目的としていましたが、２文間の語順の並び替えの距離を計ることができるので、ある言語間の文法的な距離を定量的に計るというのもできるのかなと思ったりしています