Djangoでボット的なものを作りたい話

スポンサーリンク

こんにちは。

駆け出しエンジニアの:DaVです。

11月に入り秋らしい良い天候が続いており、とてもクリエイティブになる今日この頃です。

実現できるかは別としてやりたいことに挑戦するというのはとても気持ちの良いことですよね!

ということで今回はおもむろに「Django」入門してみた話を備忘録として書きたいと思います:D

なんとなく女子高生AIの「りんな」ちゃん的なものを作ってみたいな~というノリでボット開発に挑戦してます!

入門者レベルの拙い記事ですが、「これからpythonでなんかwebアプリ作りたい」みたいなことを考えている同志の方に届いたら幸いです。

結構長めの記事なので、最終的にどんなものを作ったかだけ見たい人は目次から「ボットデモ」まで読み飛ばしちゃってください:)

それでは早速!

Djangoでボット作成

今回のゴール

まずは右も左も分からない状態なので、デザインや精度はさておいて入力に対して何かしら返信するWebアプリが作れればOKとしました:D

要するに「決められた言葉しか返さないシンプルなボット」ですね。

ただし勉強を兼ねてフレームワークは「Django」、入出力値の判定には「MeCab」や「Word2vec」を使用するって感じです。

判定部分は適当でもいいのでとりあえず類似度を計算できてればOKくらいのゆるいノリです:D

ツールの概要

準備に入る前に、今回使用するツールの概要と用途を簡単にまとめました:)

Django(ジャンゴ)とは?

「Django」とはPythonで実装されたWebアプリケーションフレームワークです。(wikiより引用)

説明通りPythonベースでいい感じにWebアプリ開発ができるフレームワークですね。

「Youtube」や「Instagram」なんかもDjangoで作成されているそうです:)

特徴としては、下記の三点がよく挙げられているみたいです。

  • フルスタック
  • 高セキュリティ
  • 学習コストが低い

私はほかのフレームワークを使用したことがなかったので、今回作成するボットで機械学習的なノリを挟みたいがために「機械学習といえばPython」という安易な発想からDjangoを選択しました:D

勢いは大事ですよね。

形態素解析ライブラリ MeCab

「MeCab」はオープンソースの形態素解析エンジンです。(wikiより引用)

形態素解析は文章を意味を持つ最小単位で分割して品詞とかの判別を行うやつです。

ボットで会話の流れに適したいい感じの返事をするには、入力された文字列(文章)がどんな意味を持っているかを解析する必要があります。

後述する「Word2vec」と連携するため「MeCab」にて文章を分割するって感じですね:D

Word2vecで自然言語処理

「Word2vec」は単語の埋め込みを生成するために使用される一連のモデル群です。(wikiより引用)

…うん。よく分からんです:D

なんか文章間の類似度を求めるための手法らしいです。

先ほどMeCabで文章の意味を解析しましたが、ボットでは結局なんて返事するのが正解なのかを決める必要があります。

シンプルに入力文章に意味が似ている文章を参照したり、尋ねられている、挨拶されている等のパターンを判別したりするためにこの「Word2vec」が必要になってくるわけですね:D

「〇〇に似てる文章であれば□□と答える」的な感じで、あらかじめ回答を用意しておけば、類似度さえ求められればそれっぽい返事ができるよって話です:)

「何をもとに類似度を計算するの?」と思った方もいるかと思いますが、「学習済みモデル」をもとに算出しているわけらしいです:D

入門者はとりあえず「Wikipedia 日本語コーパス」を用いた学習済みモデルを使っとけばいい的な記事を見かけたので、私もそれでいきたいと思います。

なんにせよMeCabで形態素解析した文章をWord2vecに流しこみ類似度を出す流れがボットのキモになりそうですね。

アルゴリズムの詳細については勉強中なので共有できることはないです…すみません:(

気になった方は調べてみてください!

開発環境の準備

ということでいよいよ準備編です。

下記前提で諸々をインストールしていきます!

  • OS: Windows10 Home 64bit
  • 実行環境:Visual Studio Code 1.62.1

Visual Studio Code」はMicrosoftが提供している無料のエディタです。

使いやすいのでオススメです:D

①Pythonをインストール

公式サイトからWindows版のインストーラをダウンロードします。

私はバージョン 3.9.6 を使用しています!

インストーラを実行しそのまま進めるだけで導入終わりです。

コマンドプロンプトで動作確認してみましょう!

python --version
> Python 3.9.6

バージョンが表示されたらOKです:D

表示されない場合はコチラの記事が分かりやすく説明してるので参考にするとよさそうです!

②Pipenvをインストール

Windowsで「Django」を使用する際には仮想環境上で実行する必要があるらしいので「Pipenv」をインストールします。

pip install pipenv

ちなみにPipenvはPythonのパッケージ管理ツールです。

適当な作業用フォルダを作成しその中でpipenvに入ります!

mkdir ws         #適当な作業用フォルダを作成
cd ws            #作成したフォルダに移動
pipenv shell     #このコマンドで pipenv(仮想環境)に入る

以降のインストールや実行はpipenv上で行います:D

③Djangoをインストール

次に「Django」をインストールします。

pip install django

コマンド一発なので楽ちんですね:D

インストールしたらプロジェクトを作成しておきましょう!

django-admin startproject projectname

プロジェクト名はなんでも良いです。

cd projectname 
python manage.py runserver

上記コマンドを実行後「http://127.0.0.1:8000」にアクセスしてぽい画面が出ればOKです。

④MeCabをインストール

次に「MeCab」をインストールします。

まずはこちらの先駆者様が作成したインストーラをダウンロードし実行します。

Pythonで使う前提でしたので、文字コードは「UTF-8」でインストールしました:D

インストールが完了したら環境変数にbinまでのパスを追加しておきます。

デフォルトだと「C:\Program Files\MeCab\bin」だと思います。

パスを通したら確認します:D

mecab --version
> mecab of 0.996

無事に動いたらPython用のライブラリをインストールします。

pip install mecab-python3

MeCabはこれにて準備完了です:D

⑤Word2vec(Gensim)をインストール

次に「Word2vec」をインストールします。

ライブラリ自体は下記コマンドで一発です:D

pip install gensim

今回使用する学習モデルはコチラの記事を参考に用意しました。

⑥その他必要ライブラリをインストール

ベクトル計算用に「Numpy」、また今後に備えて「Pandas」をインストールします。

pip install numpy
pip install pandas

これにて準備完了!

疲れました:(

ボットを実装

準備が整ったのでもろもろ実装していきます!

ディレクトリ構成とか対象はこんな感じです↓

基本設定

まずはurls.pyでルーティング設定をします。

「http:localhost:8000/davinboy」にアクセスしたらdavinboy.pyのinitを呼び出すようにして、ajax用のルーティングも行いました。

from django.contrib import admin
from django.urls import path
from . import davinboy # 追加

urlpatterns = [
    path('admin/', admin.site.urls),
    path('davinboy/', davinboy.init, name='davinboy'), # 追加
    path('davinboy/ajax/', davinboy.ajax_response),    # 追加
]

ルーティング先のdavivnboy.pyを作成します。

とりあえずシンプルにtemplate配下のhtmlを表示するだけの処理を書いてます。

from django.shortcuts import render
from django.http import HttpResponse

def init(request):
    return render(request, 'davinboy.html')

次にsettings.pyでパスの追加をします。(ディレクトリ構成通りにフォルダは作っておきます。)

import os
… 省略
TEMPLATES = [
    {
        'BACKEND': 'django.template.backends.django.DjangoTemplates',
        'DIRS': [
            os.path.join(BASE_DIR, 'templates'), # templates のパス追加
        ],
… 省略
STATIC_URL = '/static/'

STATICFILES_DIRS = (
    [
        os.path.join(BASE_DIR, "static"), # static のパス追加
    ]
)

また、formを使用しないでajaxをしたかったので、ひとまずCSRFトークンを無効化しました。

MIDDLEWARE = [
    'django.middleware.security.SecurityMiddleware',
    'django.contrib.sessions.middleware.SessionMiddleware',
    'django.middleware.common.CommonMiddleware',
    # 'django.middleware.csrf.CsrfViewMiddleware', ここをコメントアウト(非推奨)
    'django.contrib.auth.middleware.AuthenticationMiddleware',
    'django.contrib.messages.middleware.MessageMiddleware',
    'django.middleware.clickjacking.XFrameOptionsMiddleware',
]

一括無効化は非推奨らしいのでもし本格的にやる場合は用検討ですね。。。

入出力メッセージ判定部分の実装

ajaxでメッセージをPOSTされる前提で判定部分を実装します。

  • 入力:文字列(入力メッセージ)
  • 出力:文字列(返事用メッセージ)

davinboy.pyにajax用の関数を追記します。

とりあえずメッセージ作成用モジュールとのブリッジ的な感じにしてます。

...省略
from . import botMessage # 追加
...省略
# ajax用関数を追加
def ajax_response(request):
    input_text = request.POST.getlist("inData")
    outData = str(botMessage.createMessage(input_text[0]))
    return HttpResponse(outData)

メッセージ作成用のモジュールを作成します。

botMessage.pyという名前にしました:D

import MeCab
from gensim.models import KeyedVectors
import numpy as np
mt = MeCab.Tagger('')
wv = KeyedVectors.load_word2vec_format('./wiki.vec.pt', binary=True)

# 入力メッセージを分類するためのリスト
exList = [
    'よろしくお願いします',     # 挨拶
    '調子はどうですか?',       # 疑問
    'これって凄く良いよね?',   # 付加疑問
    'どうして泳げないの?',     # 否定疑問
    '機能テレビを見たんだ',     # 平叙
    '楽しそうに歌っていたんだ', # 平叙ポジティブ
    '悲しそうな目をしていたんだ', # 平叙ネガティブ
    '早く行きなさい',          # 命令
    'なんて美しいのだろう',    # 感嘆ポジティブ
    'なんて醜いのだろう'       # 感嘆ネガティブ
    ]
# 返事用のメッセージリスト
messageList = [
    'うす',
    'わからないっす',
    '僕もそう思うよ!',
    'なんでそういう事いうの…',
    'へぇー',
    'いいじゃん!',
    'ダメじゃん!',
    'だが断る',
    'そうだね!',
    '品がないぞ…'
    ]

# ベクトル計算
def getVec(text):
    sum_vec = np.zeros(200)
    node = mt.parseToNode(text)

    word_count = 0
    while node:
        fields = node.feature.split(",")
        if fields[0] == '名詞' or fields[0] == '動詞' or fields[0] == '形容詞':
            sum_vec += wv[node.surface]
            word_count += 1

        node = node.next

    if word_count > 0:
        return sum_vec / word_count
    else :
        return sum_vec

# 類似度計算
def getLike(v1, v2):
    fraction = np.dot(v1, v2)
    notreg = -10
    denominator = (np.linalg.norm(v1) * np.linalg.norm(v2))
    if denominator > 0:
        return fraction / denominator
    else :
        return notreg

def createMessage(inText):
    inTextVec = getVec(inText)
    likeList = []

    # Get Example text vector.
    for exp in exList:
        expVec = getVec(exp)
        like = getLike(inTextVec, expVec)
        likeList.append(like)

    idx = likeList.index(max(likeList))

    return messageList[idx]

入力メッセージがexListの10種類の文章パターンのうちどれに一番近しいかを「MeCab」「Word2vec」を使用して計算してます。

で、パターンに対応したあらかじめ登録していた返事用メッセージを返すという流れです。

とりあえず簡易版なのでグローバルに諸々定義しちゃってますが、DBとか活用したりもっと綺麗にしたいところです:)

適当なボット画面の実装

メイン画面となるHTML/CSSを作成します。

ライン風をイメージし、まずは入出力が見えれば良しとしました:D

{% load static %}
<!DOCTYPE html>
<html lang="ja">
<head>
  <meta charset="UTF-8">
  <title>davinboy Bot</title>
  <link rel="stylesheet" href="{% static 'css/style.css' %}">
</head>
<body>
  <div id="top-menu">
    <h1>davinboyとお話ししよう</h1>

    <form name="name_form" action="ajax/" method="POST">
      <!-- {% csrf_token %} -->
      <input type="text" id="id_input_text" name="inData" value="こんにちわ">
      <input class="btn" type="submit">
    </form>

    <button id="start_btn">話しかける</button>
    <button id="stop_btn">話すのをやめる</button>
  </div>
  <div id="content"></div>

  <script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.3.1/jquery.min.js"></script>

  <script>
    const start_btn = document.getElementById('start_btn');
    const stop_btn = document.getElementById('stop_btn');
    const content = document.getElementById('content');
    const formval = document.getElementById('id_input_text');
    const reqImgPath = "{% static 'media/myico.png' %}";
    const resImgPath = "{% static 'media/davico.png' %}";
    const speech = new webkitSpeechRecognition();
    speech.lang = 'ja-JP';

    function createMessage(message, reqflag) {
      if (reqflag == true) {
        content.innerHTML += '<div class="chat_req_div"><div class="req_child"><div>' + message + '</div><img src=' + reqImgPath + ' /></div></div>';
      } else {
        setTimeout(() => {
          content.innerHTML += '<div class="chat_res_div"><div class="res_child"><img src=' + resImgPath + ' /><div>' + message + '</div></div>';
        }, 500);
      }

      // auto scroll
      let point = content.clientHeight;
      $('html, body').animate({
        scrollTop: point
      }, 500);
    }

    function pushText(sound) {
      $.ajax({
        url: 'ajax/',
        method: 'POST',
        data: { 'inData': sound },
        timeout: 10000,
        dataType: "text",
      })
        .done(function (data) {
          $("#id_div_ajax_response").text(data);
          createMessage(data, false);
        })
    }

    start_btn.addEventListener('click', function () {
      speech.start();
    });
    stop_btn.addEventListener('click', function () {
      speech.stop();
    });

    speech.onresult = function (e) {
      speech.stop();
      if (e.results[0].isFinal) {
        var autotext = e.results[0][0].transcript
        formval.value = autotext;
        createMessage(autotext, true);
        pushText(autotext);
      }
    }

    $("form").submit(function (event) {
      const formval = document.getElementById('id_input_text');
      createMessage(formval.value, true);
      event.preventDefault();
      var form = $(this);
      console.log(form.serialize());
      $.ajax({
        url: form.prop("action"),
        method: form.prop("method"),
        data: form.serialize(),
        timeout: 10000,
        dataType: "text",
      })
        .done(function (data) {
          $("#id_div_ajax_response").text(data);
          const content = document.getElementById('content');
          createMessage(data, false);
        })
    });
  </script>
</body>
</html>

@charset "UTF-8";

html, body {
  margin: 0;
  padding: 0;
  overflow-y: auto;
}

body {
  position: fixed;
  width: 100vw;
  height: 100vh;
  background-image:radial-gradient(#FFFFFF 2px, transparent 2px), radial-gradient(#FFFFFF 2px, #adb9ca 2px);
  background-size: 40px 40px;
  background-position: 0 0,20px 20px;
  letter-spacing: .025em;
  line-height: 1.8;
  z-index: -1;
}

h1 {
  text-align: center;
  color: #333333;
}

#top-menu {
  position: sticky;
  z-index: 10;
  top: 0;
  margin: 10px;
  background-color: rgba(255,255,255,0.85);
  box-shadow: 0 0 15px 15px rgba(255,255,255,0.85);
}

.chat_res_div {
    display: flex;
}

.res_child {
  display: flex;
  align-items: center;
  justify-content: center;
}

.res_child > div {
  position: relative;
  color: #333333;
  background-color: #FFFFFF;
  padding: 15px;
  border-radius:  5px;
  font-size: 24px;
  box-shadow: 0 2px 10px 0 #9E9E9E;
  margin : 1.5em 0 1.5em 15px;
  overflow-wrap: break-word;
}

.res_child > div:before {
  content: "";
  position: absolute;
  top: 50%;
  left: -30px;
  margin-top: -15px;
  border: 15px solid transparent;
  border-right: 15px solid #FFFFFF;
}

.res_child > img {
  display: flex;
  height: 60px;
  margin: 10px;
}

.req_child > img {
  display: flex;
  height: 60px;
  margin: 10px;
}

.chat_req_div {
   display: flex;
   justify-content: flex-end;
}

.req_child {
  display: flex;
  align-items: center;
  justify-content: center;
}

.req_child > div{
  position: relative;
  color: #333333;
  background-color: #FFFFFF;
  padding: 15px;
  border-radius: 5px;
  font-size: 24px;
  box-shadow: 0 2px 10px 0 #9E9E9E;
  margin : 1.5em 15px 1.5em 0;
  overflow-wrap: break-word;
}

.req_child > div:before {
  content: "";
  position: absolute;
  top: 50%;
  left: 100%;
  margin-top: -15px;
  border: 15px solid transparent;
  border-left: 15px solid #FFFFFF;
}

アイコン画像は「ICOOON MONO」の素材を利用して作成しています。

ちなみにちょっと欲張って音声入力機能も実装してます:D

csrfトークン無効化の理由はここにあったわけです。。。

ボットデモ

ということで、最終的な成果物はこんな感じになります。

絶妙に物足りない感がありますがまあ最初はこんなもんですよね?

伸びしろしかない!

ということでひとまず簡易版の出来上がり!

おわりに

いかがでしたでしょうか!

Web系は手軽かつ視覚的にも分かりやすくて楽しいので、モチベーションも保ちやすくて良いですね!

まだ全然これからって感じですが、なんとなくボットの作り方的なものが見えてきたので、今後も少しずつ作業を進めていけたらいいなぁと思ってます:D

ボット以外にも今回の技術を応用していろいろ作ってみたいものがあるので、つまみ食いしつつマイペースで開発していく所存です。

また進捗あり次第更新します:)

長い記事になってしまいましたが、ここまで読んでいただきありがとうございました!

もしよろしければ皆さんも一緒に楽しみましょう:D

それでは、また!

コメント

タイトルとURLをコピーしました