目次
こんにちは。sinyです。
この記事は、下記記事【ディープラーニング+Djangoで言語翻訳デモアプリの作成【後編】】の後編です。
Seq2Seqモデルで学習させた英語→日本語翻訳モデルをDjangoに組み込み、下記動画のようなWEBアプリを開発していきます。
ソースはすべて以下のGitサイトにUPしてありますので、実際に試してみたい方はご利用ください。
開発環境
今回は以下のモジュール構成で開発しました。
- Django==2.2
- django-bootstrap4==0.0.8
- django-widgets-improved==1.5.0
- Keras==2.2.4
- Pillow==6.0.0
- tensorflow==1.12.0
Gitサイトにある requirements.txtを使って「pip install -r requirements.txt」すればすべて自動でインストールできます。
プロジェクトとアプリは、以下の名前で作成しています。
django-admin startproject translate
python manage.py startapp app1
settings.pyの設定
settings.pyでは以下の設定を行います。
- 必要なモジュール/アプリの追加(※1)
- テンプレートフォルダの設定(※2)
- 静的ファイルの設定(※3)
INSTALLED_APPS = [ 'django.contrib.admin', 'django.contrib.auth', 'django.contrib.contenttypes', 'django.contrib.sessions', 'django.contrib.messages', 'django.contrib.staticfiles', 'bootstrap4', #add (※1) 'widget_tweaks', #add (※1) 'app1', #add (※1) ] TEMPLATES = [ { 'BACKEND': 'django.template.backends.django.DjangoTemplates', 'DIRS': [os.path.join(BASE_DIR,'templates')], #変更 (※2) 'APP_DIRS': True, 'OPTIONS': { 'context_processors': [ 'django.template.context_processors.debug', 'django.template.context_processors.request', 'django.contrib.auth.context_processors.auth', 'django.contrib.messages.context_processors.messages', ], }, }, ] #静的ファイルの設定((※3) STATICFILES_DIRS = [ os.path.join(BASE_DIR, 'static'), ]
URLパターンの設定
プロジェクトとアプリケーションのurls.pyの設定をそれぞれ行います。
ここは特に難しい点はないかと思います。
■translate\urls.py
from django.contrib import admin from django.urls import path, include #add urlpatterns = [ path('admin/', admin.site.urls), path('app/', include('app1.urls')), #add ]
■app1\urls.py
from django.urls import path from . import views urlpatterns = [ path('demo/',views.translate, name='demo'), ]
フォームの設定
■forms.py
翻訳画面のフォーム設定です。
from django import forms class UserForm(forms.Form): messages = forms.CharField(label='英文入力',max_length=500, min_length=1,widget=forms.Textarea(attrs= {'id': 'messages','placeholder':'ここに翻訳したい英文を入力してください\n'}) )
上記で下図の赤枠の画面フォームを設定しています。
コンフィグ設定
config.pyでは、学習モデルやサンプルサイズ等の固定値の設定をまとめています。
import os from django.conf import settings NUM_SAMPLES = 10000 # サンプル数 S2S_MODEL = os.path.join(settings.BASE_DIR, 'models\s2s.h5') ENCODER_MODEL = os.path.join(settings.BASE_DIR, 'models\encoder_model.h5') DECODER_MODEL = os.path.join(settings.BASE_DIR, 'models\decoder_model.h5') DATA_PATH = os.path.join(settings.BASE_DIR, 'models\jpn.txt')
上記で指定しているモデルファイル類は、translate\modelsフォルダを作成してファイルを配置しておきます。
配置するファイルは、下記記事で作成したものを配置します。
- decoder_model.h5 (推論用の学習済みデコーダモデル)
- encoder_model.h5(推論用の学習済みエンコーダモデル)
- jpn.txt(学習に利用した英語と日本語データ)
- s2s.h5(学習済みseq2seqモデル)
ファンクション関数の設定
function.pyでは以下の関数を定義しています。
- load_text (コーパスファイルjpn.txtのロード)
- get_char (以下の情報を抽出する関数。)
input_texts(英語文)
target_texts(日本語文)
input_characters(英語の単語の種類)
target_characters(日本語の単語の種類) - get_num_word(jpn.txtに格納されている英語、日本語のトークン数、最大長の値を取得する関数)
- sentence_to_vector(文章をone-hot表現に変換する関数)
- load_models(モデルをロードする関数)
- create_dict(英語と日本語の辞書データを作成する関数)
- is_invalid(使用できない文字があったときに無効な文字を判定する関数)
- decode_sequence(入力に英文の状態ベクトルを与えて、出力(日本語訳)を出力する関数)
from app1.config import NUM_SAMPLES, S2S_MODEL, DECODER_MODEL, ENCODER_MODEL import numpy as np import tensorflow as tf from keras.models import load_model def load_text(fname): with open(fname, 'r', encoding='utf-8') as f: lines = f.read().split('\n') return lines def get_char(lines): input_texts = [] #英語のデータ target_texts = [] #日本語のデータを格納 input_characters = set() #英文に使われている文字の種類 target_characters = set() #日本語に使われている文字の種類 for line in lines[: min(NUM_SAMPLES, len(lines) - 1)]: input_text, target_text = line.split('\t') # ターゲット分の開始をタブ「\t」で、終了を改行「\n」で表す。 target_text = '\t' + target_text + '\n' input_texts.append(input_text) target_texts.append(target_text) for char in input_text: if char not in input_characters: input_characters.add(char) for char in target_text: if char not in target_characters: target_characters.add(char) #ソート処理 input_characters = sorted(list(input_characters)) target_characters = sorted(list(target_characters)) return input_texts, target_texts, input_characters, target_characters def get_num_word(i_char, t_char, i_txt, t_txt): num_encoder_tokens = len(i_char) #インプット(英語)の単語数 num_decoder_tokens = len(t_char) #アウトプット(日本語)の単語数 max_encoder_seq_length = max([len(txt) for txt in i_txt]) #一番長い英文の数 max_decoder_seq_length = max([len(txt) for txt in t_txt]) #一番長い日本語分の数 return num_encoder_tokens, num_decoder_tokens, max_encoder_seq_length, max_decoder_seq_length # 文章をone-hot表現に変換する関数 def sentence_to_vector(sentence, max_encoder_seq_length, num_encoder_tokens, input_token_index): vector = np.zeros((1, max_encoder_seq_length, num_encoder_tokens)) for j, char in enumerate(sentence): vector[0][j][input_token_index[char]] = 1 return vector def load_models(): model = load_model(S2S_MODEL, compile=False) encoder_model = load_model(ENCODER_MODEL) decoder_model = load_model(DECODER_MODEL) graph = tf.get_default_graph() return model, encoder_model, decoder_model, graph def create_dict(i_chars, t_chars): #英語辞書データの生成 input_token_index = dict([ (char, i) for i, char in enumerate(i_chars) ]) #日本語辞書データの生成 target_token_index = dict([ (char, i) for i, char in enumerate(t_chars) ]) return input_token_index, target_token_index #使用できない文字(コーパスにない文字)があったときに無効な文字を判定するために使う。 def is_invalid(message,i_chars): is_invalid =False for char in message: if char not in i_chars: is_invalid = True return is_invalid def decode_sequence(input_seq, num_decoder_tokens, target_token_index, encoder_model,\ decoder_model, reverse_target_char_index, max_decoder_seq_length, graph): with graph.as_default(): # 入力文(input_seq)を与えてencoderから内部状態を取得 states_value = encoder_model.predict(input_seq) # 長さ1の空のターゲットシーケンスを生成 target_seq = np.zeros((1, 1, num_decoder_tokens)) # ターゲットシーケンスの最初の文字に開始文字であるタブ「\t」を入力 target_seq[0, 0, target_token_index['\t']] = 1. # シーケンスのバッチのサンプリングループ # バッチサイズ1を想定 stop_condition = False # 初期値として返答の文字列を空で作成。 decoded_sentence = '' while not stop_condition: with graph.as_default(): output_tokens, h, c = decoder_model.predict([target_seq] + states_value) # トークンをサンプリングする # argmaxで最大確率のトークンインデックス番号を取得 sampled_token_index = np.argmax(output_tokens[0, -1, :]) # Index空文字を取得 sampled_char = reverse_target_char_index[sampled_token_index] # 返答文字列にサンプリングされた文字を追加 decoded_sentence += sampled_char # 終了条件:最大長に達するか停止文字を見つける。 if (sampled_char == '\n' or len(decoded_sentence) > max_decoder_seq_length): stop_condition = True # ターゲット配列(長さ1)を更新 # 長さ1の空のターゲットシーケンスを生成 target_seq = np.zeros((1, 1, num_decoder_tokens)) #予測されたトークンの値を1にセットし次の時刻の入力にtarget_seqを使う。 target_seq[0, 0, sampled_token_index] = 1. # 内部状態を更新して次の時刻の入力に使う。 states_value = [h, c] return decoded_sentence
ビューの設定
以下はビューの設定です。
import numpy as np import pickle from django.http.response import HttpResponse from django.shortcuts import render, render_to_response from django.contrib.staticfiles.templatetags.staticfiles import static from . import forms from django.template.context_processors import csrf from app1.config import NUM_SAMPLES, S2S_MODEL, ENCODER_MODEL, DECODER_MODEL, DATA_PATH from app1.function import load_text, get_char, get_num_word, sentence_to_vector,\ load_models, create_dict, decode_sequence, is_invalid from keras.models import load_model #モデル、GRAPHのロード model, encoder_model, decoder_model, graph = load_models() #コーパスファイルのロード lines = load_text(DATA_PATH) #英語、日本語文章に分け、各単語の種類を取得 input_texts, target_texts, input_characters, target_characters = get_char(lines) #インプット、アウトプットの単語数、最大長を取得 num_encoder_tokens, num_decoder_tokens, max_encoder_seq_length, max_decoder_seq_length = get_num_word\ (input_characters, target_characters, input_texts, target_texts) #英語,日本語辞書データの生成 input_token_index, target_token_index = create_dict(input_characters, target_characters) #逆引き辞書の生成 reverse_input_char_index = dict((i, char) for char, i in input_token_index.items()) reverse_target_char_index = dict((i, char) for char, i in target_token_index.items()) # 応答用の辞書を組み立てて返す def __makedic(k,txt): return { 'k':k,'txt':txt} def translate(request): if request.method == 'POST': # テキストボックスに入力されたメッセージ input_seq = request.POST["messages"] if not is_invalid(input_seq, input_characters): form = forms.UserForm() vec = sentence_to_vector(input_seq, max_encoder_seq_length, num_encoder_tokens, input_token_index) rets = decode_sequence(vec,num_decoder_tokens,target_token_index,encoder_model,\ decoder_model, reverse_target_char_index,max_decoder_seq_length, graph) # 描画用リストに最新のメッセージを格納する talktxts = [] talktxts.append(__makedic('b',input_seq)) talktxts.append(__makedic('ai', rets)) # 過去の応答履歴をセッションから取り出してリストに追記する saveh = [] if 'hist' in request.session: hists = request.session['hist'] saveh = hists for h in reversed(hists): x = h.split(':') talktxts.append(__makedic(x[0],x[1])) # 最新のメッセージを履歴に加えてセッションに保存する saveh.append('ai:' + rets) saveh.append('b:'+ input_seq) request.session['hist'] = saveh c = { 'form': form, 'rets':rets, 'talktxts':talktxts } else: err_message = "無効な文字列が含まれています。英文を入力してください。" form = forms.UserForm() c = { 'form': form, 'err_message':err_message, } c.update(csrf(request)) return render(request,'app1/demo.html',c) else: # 初期表示の時にセッションもクリアする request.session.clear() # フォームの初期化 form = forms.UserForm(label_suffix=':') c = {'form': form, } c.update(csrf(request)) return render(request,'app1/demo.html',c)
translate関数で、画面から渡ってきた英文情報を受け取り、sentence_to_vectorでベクトル化した後、decode_sequence関数に渡して予測結果を受け取ります。
以下の部分は、過去に実行した英語→日本語翻訳結果を履歴情報として保持し、画面に履歴情報を表示させるための処理です。
request.session['hist']に質問文を応答文の情報を蓄積しています。
# 描画用リストに最新のメッセージを格納する talktxts = [] talktxts.append(__makedic('b',input_seq)) talktxts.append(__makedic('ai', rets)) # 過去の応答履歴をセッションから取り出してリストに追記する saveh = [] if 'hist' in request.session: hists = request.session['hist'] saveh = hists for h in reversed(hists): x = h.split(':') talktxts.append(__makedic(x[0],x[1])) # 最新のメッセージを履歴に加えてセッションに保存する saveh.append('ai:' + rets) saveh.append('b:'+ input_seq) request.session['hist'] = saveh
テンプレートの設定
translate\templates\app1を作成し、base.htmlとdemo.htmlを作成します。
■base.html
<!doctype html> <html lang="jp"> <head> <!-- Required meta tags --> <meta charset="utf-8"> <meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no"> <!-- Bootstrap CSS --> <link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.2.1/css/bootstrap.min.css" integrity="sha384-GJzZqFGwb1QTTN6wy59ffF1BuGJpLSa9DkKMp0DgiMDm4iYMj70gZWKYbI706tWS" crossorigin="anonymous"> <script src="https://code.jquery.com/jquery-3.3.1.slim.min.js" integrity="sha384-q8i/X+965DzO0rT7abK41JStQIAqVgRVzpbzo5smXKp4YfRvH+8abtTE1Pi6jizo" crossorigin="anonymous"></script> {% block header %} {% endblock %} <title>{% block title %}デモ用テンプレート{% endblock %}</title> </head> <body> {% block content %} {% endblock %} <!-- Optional JavaScript --> <!-- jQuery first, then Popper.js, then Bootstrap JS --> <script src="https://cdnjs.cloudflare.com/ajax/libs/popper.js/1.14.6/umd/popper.min.js" integrity="sha384-wHAiFfRlMFy6i5SRaxvfOCifBUQy1xHdJ/yoi7FRNXMRBu5WHdZYu1hA6ZOblgut" crossorigin="anonymous"></script> <script src="https://stackpath.bootstrapcdn.com/bootstrap/4.2.1/js/bootstrap.min.js" integrity="sha384-B0UglyR+jN6CkvvICOB2joaf5I4l3gm9GU6Hc1og6Ls7i6U/mkkaduKaBhlAXv9k" crossorigin="anonymous"></script> </body> </html>
■demo.html
翻訳結果を表示するテンプレートです。
{% extends '../base.html' %} {% load bootstrap4 %} {% load widget_tweaks %} {% load static %} <link rel="stylesheet" href="{% static "css/talkarea.css" %}"> {% block title %} 英語→日本語翻訳機能デモ {% endblock %} {% block content %} <div class="container"> <form action="" method="post">{% csrf_token %} <h1>英語→日本語翻訳機能デモ</h1> <div class="form-group row my-4"> <label class="col-lg-2 col-form-label"><h4>{{form.messages.label}}</h4></label><br> <div class="col-lg-6"> {{form.messages|add_class:"form-control"}} </div> <div class="col-lg-2"> <a href = "http://localhost:8000/app/demo/">入力文クリア</a> </div> </div> <div class="col-lg-2"> <button type="submit" class="btn btn-primary">実行</button> </div> </form> <hr> <div id="resultarea"></div> {% if rets %} <h4>翻訳結果</h4><br> <div id="talkarea"> {% for talktxt in talktxts %} {% if talktxt.k == 'b' %} <div class="form-group row my-2"> <div class="card col-lg-5 border-secondary bg-light text-black"> {{ talktxt.txt }} </div> <div class="col-lg-1"><img src="{% static "images/question.jpg" %}" width="45" height="45" /></div> <div class="col-lg-4"></div> </div> {% else %} <div class="form-group row my-2"> <div class="col-lg-4"></div> <div class="col-lg-1"><img src="{% static "images/information.jpg" %}" width="45" height="45" /></div> <div class="card col-lg-5 border-secondary bg-primary text-white"> {{ talktxt.txt }} </div> </div> {% endif %} {% endfor %} {% endif %} {% if err_message %} <h4>入力エラー</h4><br> <div class="card mb-3 bg-danger"> <div class="card-header"> <div class="card-text text-white"> {{ err_message }} {% endif %} </div> </div> </div> </div> {% endblock %}
以下の部分で、質問文(英語)と回答文(日本語)を分けて表示しています。
{% for talktxt in talktxts %} {% if talktxt.k == 'b' %} <div class="form-group row my-2"> <div class="card col-lg-5 border-secondary bg-light text-black"> {{ talktxt.txt }} </div> <div class="col-lg-1"><img src="{% static "images/question.jpg" %}" width="45" height="45" /></div> <div class="col-lg-4"></div> </div> {% else %} <div class="form-group row my-2"> <div class="col-lg-4"></div> <div class="col-lg-1"><img src="{% static "images/information.jpg" %}" width="45" height="45" /></div> <div class="card col-lg-5 border-secondary bg-primary text-white"> {{ talktxt.txt }} </div> </div> {% endif %} {% endfor %}
staticファイルの配置
translate\static配下に、cssとimageファイルを配置しています。
翻訳結果画面で表示する画像と画面のデザインを整えるために利用しています。
■talkarea.css
#talkarea { height:600px; overflow:auto; }
マイグレーション
ここまで設定出来たら、python manage.py migrateでマイグレーションを行いましょう。
http://127.0.0.1:8000/app/demo/ にアクセスし、以下の画面が表示されればOKです。
以上で完了です。
入門者の方には少し難しい部分もあるかと思いますが、1つ1つ理解していけば構築できるようになると思いますので、ぜひチャレンジしてみてください。