"BOKU"のITな日常

62歳・文系システムエンジニアの”BOKU”は日々勉強を楽しんでます

Word2Vecで「単語の足し算・引き算の結果(単語)を取得する」デモをやってみる。

Wikipediaのテキストで学習済の「Word2Vec学習済モデル」を使った簡単な「近い言葉探し遊び」的なデモを作ってみます。

f:id:arakan_no_boku:20191229163652p:plain

 

はじめに

 

言葉(単語)をうけとって、近い言葉を結果として返すクラスは前に作りました。

arakan-pgm-ai.hatenablog.com

これを組み込んで、djangoで簡単なデモページを作ってみます。

Word2Vecモデルを扱うのに必要なパッケージなどは、上記の記事を参照します。

 

とりあえず仕様というか画面イメージ

 

初期画面はこんな感じ。

f:id:arakan_no_boku:20191229165615p:plain

単語を指定して、近い単語を探すのですが、「王+女性」みたいな単語の加算減算もやりたいので、「足す言葉」と「引く言葉」を入力欄にします。

ひとつで計算式を入力するのも考えたのですが、()の処理とかみたいな式の解析のロジックを書くのが面倒(笑)だったので、ごくシンプルにしました。

足す言葉に「王 女性」みたいに入力すると「王+女性」として扱い、引く言葉に「愛」とか入力すると「王+女性-愛」になる・・みたいな感じにします。

入力後に「送信する」ボタンを押すと、下に結果を表示する。

そういう仕様にます。

結果を表示したイメージはこちら。

f:id:arakan_no_boku:20191229171543p:plain

 

ソースコードと補足

 

修正が必要なソースの構成は以下です。

  • Word2Vec処理クラス:word2vec.py
  • djangoフォーム定義:forms.py
  • djangoテンプレート:similar.html
  • djangoコントローラ:views.py
  • djangoURL変換: urls,py 
 
Word2Vec処理クラス:word2vec.py

 

Word2Vecモデルを読み込んで、近い単語を返すクラスです。

ほぼ、こちらで作ったものを同じです。 

arakan-pgm-ai.hatenablog.com

ただ、画面仕様にあわせるため、入力のリストを受け取って、「王+女性」みたいに計算式っぽい文字列にする処理を追加してます。

import gensim


class Word2Vec:
    def __init__(self, dicpath):
        self.model = gensim.models.Word2Vec.load(dicpath)

    def get_most_similar(self, plus_list, minus_list):
        exp_str = ''
        plus = []
        for p in plus_list:
            if p in self.model.wv.vocab:
                plus.append(p)
        minus = []
        for m in minus_list:
            if m in self.model.wv.vocab:
                minus.append(m)
        try:
            if not minus:
                if not plus:
                    result = [('辞書に存在する単語がありません。', 0.00000000)]
                else:
                    exp_str = self.__exp_str(plus, '+')
                    result = self.model.most_similar(positive=plus)
            else:
                if not plus:
                    exp_str = self.__exp_str(minus, '-')
                    result = self.model.most_similar(negative=minus)
                else:
                    exp_str = self.__exp_str(plus, '+') + '-' + self.__exp_str(minus, '-')
                    result = self.model.most_similar(
                        positive=plus, negative=minus)
        except BaseException:
            result = [('処理中にエラーが発生しました。', 0.00000000)]
        return result, exp_str
    
    def __exp_str(self, p_list, dmtr):
        st = ''
        for i, p in enumerate(p_list):
            if i == 0:
                st = p
            else:
                st = st + str(dmtr) + p
        return st

補足は特にありません。 

 

djangoフォーム定義:forms.py

 

テキストボックスを2つ用意します。

from django import forms


class UserForm(forms.Form):
    textplus = forms.CharField(
        label='足す言葉',
        widget=forms.TextInput(
            attrs={
                'id': 'textplus',
                'placeholder': '単語を入力。複数の場合は空白で区切る。',
            }))

    textminus = forms.CharField(
        label='引く言葉',
        required=False,
        widget=forms.TextInput(
            attrs={
                'id': 'textminus',
                'placeholder': '入力した単語を引き算します。',
            }))

djangoのformはほぼデフォルト「必須入力」です。

当然、CharFieldもデフォルトで必須入力です。 

なので、textminusの方は「required=False」で明示的に必須をはずしてます。

 

djangoテンプレート:similar.html

 

表示画面です。

{% extends 'base.html' %}
{% load static %}
{% load bootstrap4 %}
{% load widget_tweaks %}

{% block header %}
<link rel="stylesheet" href="{% static "css/talk.css" %}"></link>
{% endblock %}

{% block title %}
  もっとも似ている言葉を探す
{% endblock %}

{% block content %}
<div class="container">
    <form action="" method="post">{% csrf_token %}
        <h4>似ている言葉を探します(言葉の加減算付き)</h4>
        <div class="form-group row my-4">         
            <label class="col-lg-3 col-form-label"><h4>{{form.textplus.label}}</h4></label>
    		<div class="col-lg-7">       
	            {{form.textplus|add_class:"form-control"}}
    		</div>
        </div>
        <div class="form-group row my-4">         
            <label class="col-lg-3 col-form-label"><h4>{{form.textminus.label}}</h4></label>
    		<div class="col-lg-7">       
	            {{form.textminus|add_class:"form-control"}}
    		</div>
        </div>
        <div class="form-group row my-4">         
    		<div class="col-lg-3">       
	            <button type="submit" class="btn btn-primary">送信する</button>
    		</div>
        </div>
        <hr/>
        <div id="resultarea">
          <div class="form-group row text-center my-2"> 
            {% if expstr %}        
	        	  <h1>計算式:「{{ expstr }}」</h1>
            {% endif %}   
	        </div> 
          <div class="form-group row text-center my-2"> 
            {% if expstr %}        
	        	  <h1>最も近い結果:「{{ result1 }}」</h1>
            {% endif %}   
	        </div> 
          <div class="form-group row text-center my-2"> 
            {% if expstr %}        
	        	  <h1>近さの度合い:「{{ result2 }}」</h1>
            {% endif %}   
	        </div> 
        </div>
        </div>
     </form>
</div>  
{% endblock %}

特に凝ったことはしていません。

 

djangoコントローラ:views.py

 

上記のHTMLテンプレートに渡す結果として

  • 計算式文字列:expstr
  • 結果の単語:result1
  • 近い確率数値:result2

の3つを定義・値のセットをしています。

from django.shortcuts import render
from . import forms
from django.template.context_processors import csrf
import re
import os
from . import word2vec as wv


def w2v_do(request):
    word2vec = wv.Word2Vec(os.path.join(os.path.dirname(os.path.dirname(__file__)), 'demos\\ja\\ja.bin'))
    if request.method == 'POST':
        # テキストボックスに入力されたメッセージ
        plusinput = request.POST["textplus"]
        minusinput = request.POST["textminus"]
        # 半角・全角空白。一応、カンマとタブも区切りにしてみた
        pluslist = re.split(r'[\s ,\t++]', plusinput)
        minuslist = re.split(r'[\s ,\t-ー]', minusinput)
        results, expstr = word2vec.get_most_similar(pluslist, minuslist)
        # 描画準備
        form = forms.UserForm(label_suffix=':')
        c = {
            'form': form,
            'expstr': expstr,
            'result1': str(results[0][0]),
            'result2': str(results[0][1])
        }
    else:
        # フォームの初期化
        form = forms.UserForm(label_suffix=':')
        c = {'form': form}
        c.update(csrf(request))
    return render(request, 'similar.html', c)

ポイントです。 

まず、views.pyから静的ファイルの学習済モデル「js/ja.bin」を読み込むところです。

クラス単体で処理するだけなら「.\\ja\\ja.bin」のように書けばよいのですが、djangoのviews.pyの中で参照するときは、それではエラーになります。

views.pyと同じフォルダに「ja」フォルダを置き、その下に「ja.bin」を置いた場合の参照方法は

os.path.join(os.path.dirname(os.path.dirname(__file__)), 'demos\\ja\\ja.bin')

になります。

例えば「demos」プロジェクトの場合だと、「os.path.join(os.path.dirname(os.path.dirname(__file__)),」で取得できるパスは以下になります。

f:id:arakan_no_boku:20191229174545p:plain

views.pyの置き場所は、上記の例だと「demos」の下になるので、ここからの相対パスである「 'demos\\ja\\ja.bin'」を書くことで、静的ファイルが取得できる・・というわけですね。

あと。

入力文字列を受け取って、単語に分解するこの部分。

pluslist = re.split(r'[\s ,\t++]', plusinput)
minuslist = re.split(r'[\s ,\t-ー]', minusinput)

いちおう。

  •  半角空白、全角空白、カンマ、タブ、半角+、全角+(足す言葉)
  •  半角空白、全角空白、カンマ、タブ、半角-、全角ー(引く言葉)

がデリミタで使えるようにしています。

それくらいですかね。

 

djangoURL変換: urls,py  

 

最後にURLです。

from django.contrib import admin
from django.urls import path
from . import views

urlpatterns = [
    path('demo06/', views.w2v_do, name='similar'),
]

例えば。 

http://localhost:8000/demo06 でアクセスする想定です。

 views.w2v_do は、views.pyの「w2v_do」関数。

name='similar' が「similar.html」をさすのは、ご存じの通りです。

 

今回のデモの実行環境とか

 

今回のデモを動かすには、Word2Vecの学習済モデルが必要です。

学習済モデルは以下からダウンロードして、この記事中で説明した所定の場所にコピーする必要があります。

github.com

djangoの環境構築や使い方等は以下などを参考にします。

arakan-pgm-ai.hatenablog.com 

今回はこんなところで。

ではでは。