[備忘録] Flask & word2vecでオススメのプログラミング言語をレコメンドしてくれるAI作ってデプロイまでしてみた

# はじめに
自然言語処理についての学習をする際に、練習として「オススメのプログラミング言語をレコメンドしてくれるAI」を作成したので作成過程を記事に残したいと思います！
こちらが完成した作品です！制作過程を残したいと思います！
https://lang-reccomend-ai-app.herokuapp.com/


モデル構築は行っていないのでそのような内容を期待されている方は読者対象ではありません🙇

記事に誤りやもっと短くかけるところなどあるかもしれないのでご指摘いただけるとありがたいです！
この記事は主に３つのセクションから成り立ちます。
[1.前処理編](#1-前処理編)
[2.Flask編](#2-Flask編)
[3.デプロイ編](#3-デプロイ編)
皆様が読みたいセクションから読んでも理解できるように記事を書いているつもりなので必要な部分だけ読んでいただいても構いません！
```HTML:開発環境
Python 3.7
macOS　Monterey
Anaconda
```
# 1.前処理編
## はじめに
　前処理編ではレコメンドするプログラミング言語をどのように比較可能なものにするかの方法を記載します。

### 手法
- 今回レコメンドする結果となる言語は筆者がインターンとして行っている[大学生限定プログラミングスクールGeekSalon](https://bit.ly/3IdnEE4)のそれぞれのコースで扱う言語＋筆者が興味ある言語に絞ります。
- それぞれの言語の詳細はQiitaの投稿から各言語のタグ名のものを１００件ほど取得し、Word2Vecを用いて集計します。
- レコメンド部分ではいくつかの選択肢をユーザーに選択させ、そのベクトルと各言語のcos類似度を計算し最も似通ったものを最終的なレコメンドとします。

それぞれの語句については実装部分で簡単な説明や参考記事を紹介するのでそちらを参照してみてください💪

## 実装
それは実装に移ります！まずは作業用の仮想環境の構築を行います！下記のコードブロックを１行ずつ実行してください！ 仮想環境名は自分で決めてくださいね！
```:terminal
>> conda create -n 任意の仮想環境名 python=3.7
>> conda activate 任意の仮想環境名
```

次に今回使用するライブラリにをインストールしましょう。各行を実行して行ってください
```Python:terminal
conda install requests
conda install pandas  #データフレームを使いたいので
pip install janome  #condaではjanomeが提供されていないようでした
conda install gensim  #word2Vecを利用するのに必要
```

それでは準備が終わったので実際にpythonのコードを書いていきたいと思います！ここからは各コードブロックの内容をlang.pyというファイルに追記して行ってください！

```python:lang.py
#ライブラリのインポート
import requests
import numpy as np
import pandas as pd
from janome.tokenizer import Tokenizer
import re #正規表現を実装
import json #QiitaAPIから渡されるjsonを処理するため
from gensim.models import KeyedVectors
```

```python:lang.py
# 最終的にレコメンドする言語の一覧（Qiitaのタグ名と一致する必要があります）
lang_list = ['Ruby','Python','Unity','Swift','JavaScript','figma','HTML','CSS','Flutter','C','Java']
df_lang = pd.DataFrame({'lang':lang_list,'body_text':'','divided':''})
```
空っぽのDataFrameを作成しました。ここにどんどん各言語の詳細を書き込んでいきます。ちなみに、この時点でのdfをprintすると以下のようになっています。
```
          lang body_text divided
0         Ruby               
1       Python               
2        Unity               
3        Swift               
4   JavaScript               
5        figma               
6         HTML               
7          CSS               
8      Flutter               
9            C               
10        Java               
```

では次にQiita APIを利用してそれぞれの言語の記事情報を取得しましょう！Qiita APIの利用は下記の２つの記事が非常に分かりやすかったので参考にさせていただきました。
[Qiita API V2 ドキュメント](https://qiita.com/api/v2/docs)
[QiitaAPIを触ってみる](https://qiita.com/miyuki_samitani/items/bfe89abb039a07e652e6)
これ以降はQiita APIを取得しているという前提で進めていきます。またコード中ではAPIキーをXで表しているので適宜ご自身のものに書き換えて実装してみてください！
```python:lang.py
# APIとheader部分を設定
qiitaToken = 'XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX'
headers = {"content-type": "application/json","Authorization": "Bearer "+qiitaToken}

# qiitaAPIで記事の本文を取得
for i in range(len(df_lang)):  #dfの行数だけfor文を実行
    res = requests.get('https://qiita.com/api/v2/tags/'+df_lang.iloc[i,0]+'/items/?per_page=100&page=5' , headers=headers)　　
    requested = res.json()
    body_text = ''

    for item in requested_list:
        if 'body' in item:
            item_body = item['body']
            body_text+=item_body
    df_lang.iloc[i,1] = body_text

```
ここで各言語に関する記事を取得できたのでここから実際に前処理を行なっていきます！主にすることは、記号・特殊文字の除去、分かち書きをして名詞・形容詞だけを抽出することです！
```python:lang.py
# 正規表現
symbol = re.compile('[!"#$%&\'\\\\()*+,-./:;<=>?@[\\]^_`{|}~「」〔〕“”〈〉『』【】＆＊・（）＄＃＠。、？！｀＋￥％]')#正規表現
table = {'\n': '','\r': '','\t':''}
```
まずは、正規表現で記号を取り除く準備をします！　ここは[Pythonの正規表現で特殊記号をすべて闇に葬り去りたいとき](https://qiita.com/ganyariya/items/42fc0ed3dcebecb6b117)を参考にさせていただきました！

次にそれぞれの文章に実際の処理を行います。
```python:lang.py
for i in range(len(df_lang)):
    text = df_lang.iloc[i,1] 
    text = text.translate(str.maketrans(table)) #記号を除去
    text = symbol.sub('', text)
    print(text)
    l = []
    t = Tokenizer()
    for token in t.tokenize(text):
        pos = token.part_of_speech.split(',') 
        if '形容詞' in pos or '名詞' in pos: #名詞と形容詞だけを抽出
            l.append(token.surface) #.surfaceで単語だけを表示（他の情報は取り込まない）
    df_lang.iloc[i,2] = l
```
これでDataFrameをprintしてみると、単語ごとに分けられたテキストが３列目に入っているのがわかるかと思います！
ちなみに、ここで名詞と形容詞だけに絞ったのは少しでも特徴量をわかりやすく出したかったからです。

この後word2vecを用いて実際に処理を行う際には、ここで分かち書きにした単語のベクトルの単純平均をとって行います。単純平均を取るとどの文章も似通ったベクトルになってしまうことがわかっているので少しでも差を出を出すために名詞形容詞に絞りました（集計方法なんとかしろよってコメントはやめてください😭😭😭）

では最後に、ここで作成したData FrameをCSVファイルに出力しておきたいと思います！ただし、分かち書きにする前の部分はいらないのでその行を削除しておきましょう〜
```python:lang.py
#CSVファイルを出力する
df_lang.drop(columns=['body_text'],inplace=True)

df_lang.to_csv('word_model.csv',index=False)

```
うまくいけばCSVファイルが出力されているかと思います！

# 2.Flask編
## はじめに
Flask編では[1.前処理編](#1-前処理編)で作成したレコメンド機能をFlaskに搭載するフェーズを行います。必要最低限の機能実装で終わらせるので単純で分かりやすいかと思います！
以下のようなフォルダ構造で作成を行なっていきます！
```:フォルダ構成
    myApp
    ├── templates
    │   ├── layout.html
    │   ├── index.html
    │   ├── new.html
    │   └── show.html
    ├── static
    │   └── css
    |       └──layout.css
    ├── app.py
    ├── word_model.csv
    ├── model.kv.vectors.npy
    ├── model.kv
    └── model.py
```

新しいフォルダにもpythonの環境を整えましょう！pythonのバージョンは3.7で行っています。
```
conda install Flask
concda install gensim
conda install pandas
```
次に実装をしていきます。
layoutには全てのページに共通して表示される部分を記載します。
```HTML:layout.html
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>プログラミング言語診断</title>
    <link rel="stylesheet" href="/static/css/layout.css">
    <link href="https://cdn.jsdelivr.net/npm/bootstrap@5.0.2/dist/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-EVSTQN3/azprG1Anm3QDgpJLIm9Nao0Yz1ztcQTwFspd3yD65VohhpuuCOmLASjC" crossorigin="anonymous">
</head>
<body>
    <header>
        <h1>ITスキル診断</h1>
    </header>
    <main>
        <!-- Jinja templateを使って挿入  -->
        {% block content %}
        {% endblock %}
    </main>
    <footer>
        <p>Copyright ©XXXX All Rights Reserved.</p>
    </footer>
    <script src="https://cdn.jsdelivr.net/npm/@popperjs/core@2.9.2/dist/umd/popper.min.js" integrity="sha384-IQsoLXl5PILFhosVNubq5LC7Qb9DXgDA9i+tQ8Zj3iwWAwPtgFTxbJ8NT4GN1R8p" crossorigin="anonymous"></script>
    <script src="https://cdn.jsdelivr.net/npm/bootstrap@5.0.2/dist/js/bootstrap.min.js" integrity="sha384-cVKIPhGWiC2Al4u+LWgxfKTRIcfu0JTxR+EQDz/bgldoEyl4H0zUF0QKbrJ0EcQF" crossorigin="anonymous"></script>
</body>
</html>
```
同様にその他のHTMLも書いていきます。
```HTML:index.html

{% extends "layout.html" %}
{% block content %}
<div class = "content">
    <a href="/new" role="button">診断する</a>
</div>
{% endblock %}
```
診断画面の選択肢は適当です。もし実装される場合は適宜修正してみてください（うまくいったのがあれば教えてください）
```HTML:new.html
{% extends "layout.html" %}
{% block content %}
<div class = "content">
    <h2>new.html</h2>
    <a href="/" role="button">トップページ</a>
    <form method="POST" action="/show">
        <div>
            <h3>あなたの性格は？</h3>
            <input type="checkbox" id="char1" value ="情熱" name="character">
            <label for="character">情熱的</label>
            <input type="checkbox" id="char2" value ="冷静" name="character">
            <label for="character">冷静</label>
            <input type="checkbox" id="char3" value ="メンヘラ" name="character">
            <label for="character">メンヘラ</label>
            <input type="checkbox" id="char4" value ="人見知り" name="character">
            <label for="character">人見知り</label>
            <input type="checkbox" id="char5" value ="平凡" name="character">
            <label for="character">平凡</label>
            <input type="checkbox" id="char6" value ="奇妙" name="character">
            <label for="character">奇妙</label>
            <input type="checkbox" id="char7" value ="愉快" name="character">
            <label for="character">愉快</label>
            <input type="checkbox" id="char8" value ="利己" name="character">
            <label for="character">利己的</label>
            <input type="checkbox" id="char9" value ="斬新" name="character">
            <label for="character">斬新</label>
            <input type="checkbox" id="char10" value ="危険" name="character">
            <label for="character">危険</label>
        </div>

        <div>
            <h3>プログラミングのイメージは？</h3>
            <input type="checkbox" id="ideal1" value ="簡単" name="ideal">
            <label for="ideal">簡単</label>
            <input type="checkbox" id="ideal2" value ="難しい" name="ideal">
            <label for="ideal">難しい</label>
            <input type="checkbox" id="ideal3" value ="便利" name="ideal">
            <label for="ideal">便利</label>
            <input type="checkbox" id="idea4l" value ="楽しい" name="ideal">
            <label for="ideal">楽しい</label>
            <input type="checkbox" id="ideal5" value ="最新" name="ideal">
            <label for="ideal">最新</label>
            <input type="checkbox" id="ideal6" value ="偉大" name="ideal">
            <label for="ideal">偉大</label>
            <input type="checkbox" id="ideal7" value ="貧弱" name="ideal">
            <label for="ideal">貧弱</label>
            <input type="checkbox" id="ideal8" value ="危険" name="ideal">
            <label for="ideal">危険</label>
            <input type="checkbox" id="ideal9" value ="単純" name="ideal">
            <label for="ideal">単純</label>
            <input type="checkbox" id="ideal10" value ="苦痛" name="ideal">
            <label for="ideal">苦痛</label>
        </div>
        <button type="submit",value="診断">診断</button>
    </form>
</div>
{% endblock %}
```
```HTML:show.html
{% extends "layout.html" %}
{% block content %}
<div class = "content">
    <h2>診断結果</h2>
    <h1>あなたにおすすめのスキルは{{ max_lang }}です！</h1>
    <a href="/new">もう一度診断してみる</a>
    <a href="/index">トップページ</a>
</div>
{% endblock %}
```

次にapp.pyにルーティングを設定していきます。
```python:app.py
from flask import Flask,request,render_template
from model import calculate_language_vector,calculate_emotion_vector,search_most_similar
app = Flask(__name__)

@app.route('/')
def index():
    return render_template('index.html')

@app.route('/new')
def new():
    return render_template('new.html')

@app.route('/show',methods=['GET',"POST"])
def result():
    if request.method == 'GET':
        return render_template('result.html')
    elif request.method == 'POST':
        char = request.form.getlist("character")
        ideal = request.form.getlist("ideal")
        key_list = char + ideal
        emothion_vector = calculate_emotion_vector(key_list)
        max_lang = search_most_similar(emothion_vector)
        return render_template("result.html",max_lang = max_lang)

if __name__ == "__main__":
    app.run()
```

最後にmodel.pyを作成します
```python:model.py
from gensim.models import Word2Vec
import pandas as pd
import numpy as np
from gensim.models import KeyedVectors
import scipy.spatial.distance

model = KeyedVectors.load('model.kv')

#ベクトルの計算
def calculate_language_vector(words):
    features = 300
    feature_vec = np.zeros((features),dtype = "float32")
    for word in words:
        try:
            feature_vec = np.add(feature_vec, model[word])
        #例外処理.辞書にない文字が出たときは処理をスキップする
        except KeyError:
            pass
    if len(words) > 0:
        feature_vec = np.divide(feature_vec, len(words))
    return feature_vec

#最も類似したものを探す
def search_most_similar(emotion_vector):
    df = pd.read_csv('word_model.csv')
    max_lang = df.iloc[0,0]
    tmp_max =0
    for i in range(len(df)):
        vect = calculate_language_vector(df.iloc[i,1])
        score = 1-scipy.spatial.distance.cosine(emotion_vector, vect)
        if score > tmp_max:
            max_lang = df.iloc[i,0]
            tmp_max = score
    return max_lang

#入力された選択肢のベクトルを計算する
def calculate_emotion_vector(key_list):
    feature_vec = np.zeros((300),dtype = "float32")
    for word in key_list:
        feature_vec = np.add(feature_vec, model[word])
    return feature_vec
```

これで実装部分は完了です。できているかどうかを確認しましょう！
```terminal:terminal
% python app.py
```
これを実行し、[http://127.0.0.1:5000/](http://127.0.0.1:5000/)にアクセスしてサイトが動作すれば完成です。

最後にデザインを施しましょう！デプロイするのですからそれなりのデザインは必須です！（コードは割愛します。）

## 補足
### word2vecとは？
ここまで利用してきたword2vecとは何かについて簡単にメモを残します。
Word2vecは単語を数値ベクトルに変換してその意味を数学的に処理することができるようにした自然言語処理の手法です。
Word2Vecは分散表現によって単語の意味を表しています。ここでいう意味は数学的に単語同士の意味の近さを図ったり演算を行ったりすることができます。
ですからWord2Vecでは　
"東京”-”日本”＋”フランス”＝”パリ”
のような演算を行うこともできます！
[参考記事](https://ainow.ai/2021/04/08/254071/)
### cos類似度とは？
cos類似度は高校数学で習った内積の考え方を応用して、２つのベク等がどのくらい似ているかという類似度を表すものになっています。
式で表すと以下のようになります。内積の式と同じですね！
```math
cos(\vec{p},\vec{q}) = \frac{\vec{p} \cdot\vec{q}}{|\vec{p}||\vec{q}|} 
```
cos類似度は-1≦cosθ≦1の範囲を取ります。
- １に近いほどそのベクトル同士が似ている
- ０に近いほどそのベクトル同士に関係がない
- −１に近いほどそのベクトル同士は逆の意味を持つ
というふうな性質になっています。

[参考記事](https://www.hanachiru-blog.com/entry/2020/03/31/120000)
# 3.デプロイ編
## はじめに
デプロイ編では[heroku](https://dashboard.heroku.com/)を用いて行います（herokuの無料プランが終わってしまうと発表されているので参考程度でお願いします！）

一通り動作することがわかったのでHerokuにデプロイします。
ここでは[heroku](https://devcenter.heroku.com/ja/articles/heroku-cli)の導入ができている前提です。
まず、ルートディレクトリに以下のファイルを作成してください！
```
Procfile
requirements.txt
runtime.txt
```
それぞれの内容を以下のように書いてください
```:Procfile
web: gunicorn app:app --log-file=-
```
```:requirements.txt
flask==2.1.3
gensim==4.2.0
gunicorn==20.1.0
numpy==1.21.5
pandas==1.3.5
scipy==1.7.3
```
requirements.txtは下記のコマンドで自動で作成することもできますが、不要な記述があったりエラーを引き起こしたりするためあまりお勧めしません
```
conda list -e > conda_requirements.txt
```

```:runtime.txt
python-3.7.13
```
この数字はterminalで以下を実行することでも得られます。
```
cat runtime.txt
```

これで準備は出来ました。ここからはterminalで作業を行います.
```terminal:terminal
git init
```
```terminal:terminal
git add .
```
```terminal:terminal
git commit -m "first commit"
```
```terminal:terminal
heroku login
```
ここでherokuへのログインを済ませてください。もし上のコマンドでログインできなければ下記のコマンドでターミナルからログインを行なってください。下記のコマンドでログインするときのパスワードはAPIキーになっているので注意してください！
```terminal:terminal
heroku login -i
```

```terminale:terminal
heroku create 任意のアプリ名 --stack heroku-18
```
アプリ名は他の人とかぶる名前は用いることができません。
```terminal:terminal
heroku stack:set heroku-18
```
```terminal:terminal
git remote 
```
herokuという文字列が返ってくるはずです。
```terminal:terminal
git push heroku master
```
これができれば完璧です。終わり次第heroku openで作成したサイトにアクセスしてみましょう！
```teminal:terminal
heroku open
```

#最後に
長い記事でしたが読んでいただいてありがとうございました。
わかりにくい記事になっていたかと思いますが少しでも誰かの役に立っていれば幸いです。
特に最後のデプロイのところは自分でも理解があやふやな部分が多いので今後勉強していこうかと思います！

誤っている箇所があればコメント等で教えていただけますと幸いです！

閲覧いただきありがとうございました！

グッバイ

# 参考記事
- https://qiita.com/api/v2/docs
- https://qiita.com/miyuki_samitani/items/bfe89abb039a07e652e6
- https://note.nkmk.me/pandas/
- https://ainow.ai/2021/04/08/254071/
- https://note.nkmk.me/python-janome-tutorial/
- https://atmarkit.itmedia.co.jp/ait/articles/2112/08/news020.html#:~:text=%E7%94%A8%E8%AA%9E%E8%A7%A3%E8%AA%AC,%E5%80%A4%E3%81%AE%E3%81%93%E3%81%A8%E3%81%A7%E3%81%82%E3%82%8B%E3%80%82
- https://qiita.com/u6k/items/5170b8d8e3f41531f08a
- https://devcenter.heroku.com/ja/articles/heroku-18-stack
- https://qiita.com/redpanda/items/a056daea48b545250ce7
- https://qiita.com/enta0701/items/87cbe783aeb44ddf41ce
- https://toukei-lab.com/python-mecab
- https://qiita.com/shimi7o/items/b3bc64e2fbe1103c7db9