千葉県Go To EAT向けに店舗検索BOT（AI LINE BOT）を作った話(4)【AIモデル作成】

<a href="">千葉県Go To EAT向けに店舗検索BOT（AI LINE BOT）を作った話(3)【python環境構築】</a>の続きです。

今回は赤字の部分のデータ収集とAIモデル作成について記載します。
![無題４.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/896062/55358a11-5920-1bc6-bcf3-08b98c7dc182.png)

##データ集め１（店舗情報）
Go To Eatクーポン券が利用可能なお店をサジェストするためには、お店データを集める必要があります。今回は、Go To Eat千葉のWEBサイトにてお店一覧がpdfファイルで記載されていたのでそれを利用しました。

- <a href="https://www.chiba-gte.jp/downloads/store_list.pdf">お店一覧のURL</a>

↑2020/11/27日現在ダウンロード出来なくなっています。

↓以下のやり方で情報が集められるようです。

- <a href"https://qiita.com/barobaro/items/02f734b84ac358911e91">参考：千葉県 Go To EATの加盟店一覧をCSVに変換</a>

↓ダウンロード出来た一覧はこんな感じ（黒いところに店舗名、住所、電話番号が載っていました）
![店一覧.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/896062/4b399a38-e601-6ee6-eea6-6493a299f7d0.png)

pdf → CSVの変換はいろいろやり方あるかと思いますが、今回は以下の方法を参考にさせていただきました。

- <a href="https://qiita.com/higa4/items/7cb4c833e383b47b2c18">参考：OpenRefineとWordでPDFと戦う</a>

##データ集め2（ジャンル情報）
今回は料理のジャンルからお店をサジェストしたいので、グルメサイトの情報を利用させていただきました。検索エンジンにてお店の名前を検索し、出てきた食べログページのジャンル情報をスクレイピングしました。プログラムは以下です。

<a href="https://github.com/Haradasn/linebot">https://github.com/Haradasn/linebot</a>

```python:scraping1.py
import csv
import time
import pprint
import requests
import urllib3
import pandas as pd
from bs4 import BeautifulSoup
import re
from requests.exceptions import Timeout
def connect_url(url,df,idx):
    stand_by_sec = 5
    try:
        print(f"\nurl: {url}\tへ接続開始...")
        u = requests.get(url, timeout=3.5)
        soup = BeautifulSoup(u.content, "lxml")
        elems = soup.find_all("a",href=re.compile('^.*tabelog.com/chiba'))　#食べログ千葉のURLを抽出する
        if not elems:
            print("Nothing")
        else:
            for elem in elems:
                if re.search(r'^https://.*/[0-9]{8}/',elem['href'][7:]) :
                    print(re.search(r'^https://.*/[0-9]{8}/',elem['href'][7:]).group())
                    urlName = re.search(r'^.*/[0-9]{8}/',elem['href'][7:]).group()
                    df[df.columns[7]][idx]= urlName
                    url = requests.get(urlName)
                    soup = BeautifulSoup(url.content, "lxml")
                    elems = soup.find_all("span",class_="linktree__parent-target-text")
                    for index,elem in enumerate(elems):
                        print(elem.text) 
                        df[df.columns[8+index]][idx]= elem.text
                    break    
    except requests.exceptions.Timeout:
        # タイムアウトした時は2秒待機して再実行
        print(f"\rタイムアウトしました...\n再接続待機中...{stand_by_sec}秒後に再実行します", end="", flush=True)
        time.sleep(stand_by_sec)
        connect_url(url=url,df=df,idx=idx)
      
    except requests.exceptions.ConnectionError:
        # タイムアウトした時は2秒待機して再実行
        print(f"\rタイムアウトしました...\n再接続待機中...{stand_by_sec}秒後に再実行します", end="", flush=True)
        time.sleep(stand_by_sec)
        connect_url(url=url,df=df,idx=idx)
        
        
    except requests.exceptions.HTTPError:
        # タイムアウトした時は2秒待機して再実行
        print(f"\rタイムアウトしました...\n再接続待機中...{stand_by_sec}秒後に再実行します", end="", flush=True)
        time.sleep(stand_by_sec)
        connect_url(url=url,df=df,idx=idx)
    except urllib3.exceptions.ReadTimeoutError:
        # タイムアウトした時は2秒待機して再実行
        print(f"\rタイムアウトしました...\n再接続待機中...{stand_by_sec}秒後に再実行します", end="", flush=True)
        time.sleep(stand_by_sec)
        connect_url(url=url,df=df,idx=idx)
    else:
        # 成功時の処理
        return u,elems
    finally:
        # 後始末
        pass
df = pd.read_csv('#千葉県の店舗一覧のCSVをロードする', header=None)
#元のcsvデータとは別に列を追加する
df = df.assign(url=0)
df = df.assign(elm1=0)
df = df.assign(elm2=0)
df = df.assign(elm3=0)
df = df.assign(elm4=0)
print(df['elm3'][11])
for idx,row in df.iterrows():
    urlName = "https://www.google.com/search?q=" +  re.sub("　","+",re.sub("&","＆",re.sub("-","",row[4])))+ "+" + row[6] + "+食べログ"
    url,elems = connect_url(urlName,df,idx)
df.to_csv("#保存先を記載する")
print("終わり！")
```
上記のプログラムで既存のcsvに最寄り駅、ジャンル情報を追加しています。

最終的に以下のカラムを持つcsvデータを作成しました。

| 市| 区 |紙クーポン可否|電子クーポン可否|店名|住所|電話番号|食べログURL|最寄り駅|ジャンル1|ジャンル2|ジャンル3|ジャンル4|
|:-:|:-:|:-:|:-:|:-:|:-:|:-:|:-:|:-:|:-:|:-:|:-:|:-:|


##AIモデルの作成
お店をサジェストするAIは、gensimのdoc2vecを用いて作成しました。
作成に当たっては以下のページを参考にさせていただきました。

<a href="https://qiita.com/akira_/items/f9bb46cad6834da32367">文章をベクトル化して類似文章の検索</a>

mecabにて形態素解析をして単語に分割したのちに、doc2vecに入力して学習させます。
店舗名をIDにジャンルや最寄り駅情報を紐づけて学習させています。

```python:train.py
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
import pandas as pd
import os
import sys
import MeCab
import collections
from gensim import models
from gensim.models.doc2vec import TaggedDocument
import re
import time
OUTPUT_MODEL = 'doc2vec.model'

# 文章から単語に分解して返す
def split_into_words(doc, name=''):
    mecab = MeCab.Tagger("-Ochasen")
    lines = mecab.parse(doc).splitlines()
    words = []
    for line in lines:
        chunks = line.split('\t')
        if len(chunks) > 3 and  (chunks[3].startswith('形容詞') or (chunks[3].startswith('名詞') and not chunks[3].startswith('名詞-数'))):
            words.append(chunks[0])
    return TaggedDocument(words=words, tags=[name])

# データから単語のリストを取得
def corpus_to_sentences(corpus):
    for idx,doc in enumerate(corpus.iterrows()):
        sys.stdout.write('\r前処理中 {} / {}'.format(idx, len(corpus)))
#        yield split_into_words(str(df[2][idx])+re.sub("駅|京成","",str(df[9][idx]))+str(df[11][idx])+str(df[12][idx])+str(df[13][idx])+str(df[5][idx]), df[5][idx])
        yield split_into_words(str(df[2][idx])+re.sub("駅|京成","",str(df[9][idx]))+str(df[11][idx])+str(df[12][idx])+str(df[13][idx])+str(df[14][idx])[:5]+str(df[5][idx]), df[5][idx])

# 学習
def train(sentences):
    model = models.Doc2Vec(dm=1,vector_size=300, sample=5e-5, min_count=0, workers=15,window=1,epochs=600,hs=1,negative=5)
    model.build_vocab(sentences)
    model.train(sentences, total_examples = model.corpus_count, epochs = model.epochs)
    ranks = []
    for doc_id in range(int(sum([len(sentence) for (sentence) in (sentences)])/2)):
        inferred_vector = model.infer_vector(sentences[doc_id].words)
        sims = model.docvecs.most_similar([inferred_vector], topn=len(model.docvecs))
        rank = [docid for docid, sim in sims].index(sentences[doc_id].tags[0])
        ranks.append(rank)
    print(collections.Counter(ranks))
    return model

if __name__ == '__main__':
    df = pd.read_csv('#CSV情報', header=None)
    sentences = list(corpus_to_sentences(df))
    start = time.time()
    model = train(sentences)
    elapsed_time = time.time() - start
    model.save(OUTPUT_MODEL)
    print ("elapsed_time:{0}".format(elapsed_time) + "[sec]")
```

モデル作成後に、検索ワードのベクトルを読み込ませ、動作を確認します。

```python:search.py
#!/usr/bin/env python3
# -*- coding: utf-8 -*-

import MeCab
import gensim.models.doc2vec as doc2vec
from gensim import models
from gensim.models.doc2vec import TaggedDocument

model = models.Doc2Vec.load('doc2vec.model')
def trim_doc(doc):
    lines = doc.splitlines()
    valid_lines = []
    is_valid = False
    horizontal_rule_cnt = 0
    break_cnt = 0
    for line in lines:
        if horizontal_rule_cnt < 2 and '-----' in line:
            horizontal_rule_cnt += 1
            is_valid = horizontal_rule_cnt == 2
            continue
        if not(is_valid):
            continue
        if line == '':
            break_cnt += 1
            is_valid = break_cnt != 3
            continue
        break_cnt = 0
        valid_lines.append(line)
    return ''.join(valid_lines)

def split_into_words(doc):
    mecab = MeCab.Tagger("-Ochasen")
    valid_doc = doc
    lines = mecab.parse(valid_doc).splitlines()
    words = []
    #print(lines)
    for line in lines:
        chunks = line.split('\t')
        if len(chunks) > 3 and  (chunks[3].startswith('形容詞') or (chunks[3].startswith('名詞') and not chunks[3].startswith('名詞-数'))):
            words.append(chunks[0])
    print("words=",words)
    return words

# 似た文章を探す
def search_similar_texts(words):
    x = model.infer_vector(words)
    #print(x)
    most_similar_texts = model.docvecs.most_similar([x])
    for similar_text in most_similar_texts:
        print(similar_text)

if __name__ == '__main__':
    print('文字列入力:')
    search_str = input()
    words = split_into_words(search_str)
    search_similar_texts(words)
```

```:実行結果
文字列入力: ハンバーガー　船橋　
('モスバーガー船橋本町店', 0.5699790716171265)
('船橋酒場ふなぞう', 0.5292383432388306)
('フレッシュネスバーガー船橋店', 0.5287526249885559)
('モスバーガー\u3000アリオ市原店', 0.4948973059654236)
('モスバーガー四街道店', 0.4904986619949341)
('フレッシュネスバーガーセブンパークアリオ柏店', 0.4869162440299988)
('ビッグテキサス船橋', 0.4765006899833679)
('和幸\u3000松戸西口店', 0.4758237600326538)
('モスバーガー\u3000イオン新浦安店', 0.4637131094932556)
('サーティワンアイスクリーム船橋イトーヨーカドー店', 0.4555884003639221)
```
なんとなくそれっぽい結果をサジェストしてくるようになりました！
これでAIモデル作成は終了です。