MENU

python データベース(RDBMS)を作成しデータを格納。

f:id:hanamichi_sukusuku:20210222185408p:plain

実行結果

hw.sqlite3というデータベースファイルが生成される。
実際の機械学習のプログラムにおいてはデータベースでのデータを使用することがほとんどなのでデータベースを連携させ、機械学習を行えるようにデータベースの生成から学んでいく。

 

上記コードでデータベースファイルの作成。

今回では簡易に利用できるデータベース(RDBMS)として、SQLiteを利用。SQLiteであれば、pythonの標準ライブラリーなので手軽にSQLクエリーを利用してデータベースを操作できる。

今回作成しているデータベースは、身長と体重、体型の3つのフィールドを持つデータベースとする。

列  フィールド説明    DBフィールド名

0  顧客ID(自動で追加)     id

1  身長(cm)        height

2  体重(kg)        weight

3  体型(0~5の値)     typeNo

 

体型は6段階の値で表現

値  体型

0   低体重(痩せ型)

1  普通体重

2  肥満1

3  肥満2

4  肥満3

5  肥満4

 

データベースに接続して、SQL文の実行

with sqlite3.connect(dbpath) as conn:
 conn.execute(sql)

 

sqlite3.connect()でデータベースに接続し、オブジェクト生成。SQL文を実行するため、execute()メソッドを使用。

 

SQL文ではデータベースを作成するために三連引用符を使用。

CREATE TABLE IF NOT EXISTS person(カラム名 データ型, カラム名 データ型...)

CREATE TABLE(テーブル作成) IF NOT EXISTS(オプション、データベースに同じ名前のテーブルがないときだけテーブルを作成する制約) person(テーブル名)。

続く()内ではカラム名とデータ型を指定している。

id(カラム名)  INTEGER(データ型) PRIMARY KEY(制約、重複した値の保存を禁止する)

height(カラム名) NUMBER

weight(カラム名) NUMBER

typeNo(カラム名) INTEGER

INTEGERとNUMBERはどちらも整数型を表すそうだが明確な違いはわからなかった。

 

次に新規に身長と体重、体型を100件追加するプログラムを実行してデータベースにデータを格納していく。

f:id:hanamichi_sukusuku:20210222194619p:plain

実行結果

f:id:hanamichi_sukusuku:20210222194641p:plain

100件の身長、体重、体型のデータがデータベースに格納される。

 

データベースに接続し、関数呼び出し

with sqlite3.connect(dbpath) as conn:
# データを100件挿入 --- (*4)
 for i in range(100):
  insert_db(conn)
# トータルで挿入した行数を調べる --- (*5)
 c = conn.execute('SELECT count(*) FROM person')
 cnt = c.fetchone()
 print(cnt[0])

sqlite3.connect()で引数にデータベースファイルのパスを指定しデータベースに接続。

for文で100回insert_db()関数を実行。insert_db()関数では身長、体重、体型のデータをデータベースに格納し、内容を出力する。

c = conn.execute('SELECT cout(*) FROM preson')ではpersonテーブルのレコードの総数を取得している。SELECT文はデータベースからデータを取り出す時に使う。count(*)で全てのレコードの総数を取得。FROM句でテーブル指定。返り値はsqlite3.Cursorオブジェクト。

c.fetchone()でcorsorオブジェクトの1行目をタプルで取得する。

 

insert_db()関数、データベースにデータを格納

f:id:hanamichi_sukusuku:20210222200915p:plain

random.randint(130, 180)で130~180までの整数をランダムに変数heightに格納。

weightも同様。

体型はbmiによって値を変更。BMIは体重(kg) / (身長(cm)/100)の2乗 。

bmi = weight / (height / 100) ** 2

**を使用することで身長/100を2乗している。

if文でBMIの値によってtype_no(体型)に格納する値を変更。

 

sql = '''
INSERT INTO person (height, weight, typeNo)
VALUES (?,?,?)
'''
values = (height,weight, type_no)
print(values)
conn.executemany(sql,[values])

conn.executemany()で一度に複数のデータを保存する。タプルをリストのを扱うので第二引数で[]で囲っている。for文を回すように一つの指令(第一引数のSQL文)を複数のデータに実行する(第二引数)。

第一引数のSQL文のINSERTではデータベースにデータを保存したい時にはINSERT文を使う。

書式

INSERT INTO テーブル名(カラム名, カラム名..)

VALUES(値, 値,..)

 

VALUES(?,?)のようにすることでexecutemany()の第二引数で指定したリストが順番に?の部分に格納され、それぞれのカラムに保存される。

 

 

 

 

 

 

 

 

webサービスにおける学習データについて

webサービスにおける学習データについて

現在では多くのサービスで機械学習システムを導入しているが、そのシステムが活躍する上で一番難しいところが「一度作って終わりではない」という点にある。日々、新しい投稿が行われ、分類精度が低下してしまうからである。流行語や新製品など、正解データは常に変化していく。そのため定期的に教師データを新しくして、学習をやり直す必要がある。webサービスでは投稿された内容をデータベースに保存する。そのデータベースに蓄積されたデータを用いて、機械学習システムを構築する。それにより、機械学習システムがwebサービスでユーザーの投稿を支援する。そして、それにより、ユーザーがさらに多くの投稿をし、それらがデータベースに蓄積されていく。そうした、蓄積されたデータを機械学習に応用すると高い精度で投稿を支援できる。

 

多くのwebサービスでは、「webサービス」→「データベース」→「機械学習システム」→「webサービス」→「データベース」....このようなフローが自動的に行われるような構築するのが効果的。何かしらのきっかけで急に判定精度が悪くなる可能性もあるので、精度が落ちたら通知が来るようにするなど、自動化の落とし穴を避ける仕組みも組み込むと良い。

 

まとめ

・学習済みデータを保存しておいて、業務システムから読み込んで使うと効率がいい

・Flaskを利用して、Pythonを利用した機械学習システムを持つWebサーバーを作成できる

機械学習システムの入出力をWeb APIとして利用できるようにしておくと便利

APIを呼び出すwebアプリの作成

f:id:hanamichi_sukusuku:20210220171930p:plain

 

index.html

f:id:hanamichi_sukusuku:20210220173647p:plain

実行結果

f:id:hanamichi_sukusuku:20210220172030p:plain

今回のプログラムでは

hanamichi-sukusuku.hatenablog.com

 

上記で作成した機械学習の機能を持つwebサーバーを使用して、APIを呼び出すwebアプリを作成していく。

webサーバーでは既にURLで渡されたパラメーターを機械学習にかけるようになっている。

今回必要なのはhtml/css, javascriptを用いたindex.htmlファイルを作成する。

 

入力欄とボタンの作成

<DOCTYPE html>
<html><meta charset="utf-8"><body>
<h1>テキストのジャンル判定</h1>
<div>
<textarea id="q" rows="10" cols="60"></textarea>
<br><button id="qButton">判定</button>
<div id="result"></div>
</div>

 

判定ボタンがクリックされたときの処理(JS)

<script>
const qs = (q) => document.querySelector(q)
window.onload = () => {
const q = qs('#q')
const qButton = qs('#qButton')
const result = qs('#result')
// 判定ボタンを押した時 --- (*1)
qButton.onclick = () => {
result.innerHTML = "..."
// APIサーバに送信するURLを構築 --- (*2)
const api = "/api?q=" +
encodeURIComponent(q.value)
// APIにアクセス --- (*3)
fetch(api).then*1で受け取りjson形式で次のthen()の処理に返り値を渡している。then()メソッドでは複数の処理を繋ぎ、順番に実行していく。

then((data)=>...ではhtmlで結果を出力するdiv要素にラベルデータと確率のデータを文字列で追加している。

 

 

 

*1:res) => {

return res.json() // JSONで返す
}).then((data) => {
// 結果を画面に表示 --- (*4)
result.innerHTML =
data["label"] +
"<span style='font-size:0.5em'>(" +
data["per"] + ")</span>"
})
}
}
</script>

const qsで無名関数を定義。引数に指定したidの要素を取得する。

qButton.onclickで判定ボタンが押されたときの処理を記述。

result.innerHTMLでAPIサーバーから出力が返ってくるまで"..."と表示する。

const apiで変数apiAPIサーバーに送信するURLを構築。encodeURIComponent()では引数に指定した文字列をURLで使用できる形式に変換(エンコード)するためのもの。URLでは日本語は使用できないのでパラメーターとして送る文字列をこれによって変換している。

fetch()メソッドでは非同期で引数に指定したURLにHTTPリクエストを送信している(デフォルトはGET)。responseオブジェクトをfetch(api)からthen((response

機械学習の機能を持つwebサーバーの作成

webサービス機械学習のシステムを組み込むためにはwebサーバーと機械学習サーバーを異なる形で動かすことやwebサーバー内で機械学習のシステムを持たせる方法になどによってwebサービス機械学習のシステムを組み込むことができる。

今回はまず、python機械学習の機能を持つwebサーバーを作成していく。

f:id:hanamichi_sukusuku:20210219192356p:plain

実行結果

f:id:hanamichi_sukusuku:20210219192419p:plain

ソースコードでは見やすさの観点からjupyter notebookでのコードになっているが実行はコマンドラインから行う。

 

実行結果では「野球を見るのは........楽しみです。」という文章をプログラムに渡して、json形式でその結果が返されている。jsonなので日本語がエンコードされていまっているがデコードすると「スポーツ」となる。

 

hanamichi-sukusuku.hatenablog.com

ここで使用しているプログラムは同じディレクトリ内にある、上記で作成したファイル(my_text.py)のcheck_genre関数を使用し、URLで渡した文章を分類している。

 

モジュールインポート

import json
import flask
from flask import request
import my_text

jsonモジュールはpythonjson形のデータを扱う時に利用する。

flaskはwebサーバーを手軽に作成できるフレームワーク

from flask import requestではURLからパラメーターの取得ができたりするためにインポート。

import my_textは上記で記述した通り、事前に作成した機械学習のプログラム。

 

ポート番号、HTTPサーバーの起動

TM_PORT_NO = 8888
# HTTPサーバを起動
app = flask.Flask(__name__)

TM_PORT_NOはポート番号で8888を定義しているがもし、別のアプリで8888番を使っている場合には他の番号に変更する。

flask.Flask(__name__)でFlaskオブジェクトを生成する。

 

ターミナル上でプログラムが正しく実行されるかのテスト

label, per, no = my_text.check_genre("テスト")
print("> テスト --- ", label, per, no)

この出力結果はターミナル上に出力される。

無事出力されたので次に進む。

 

ローカルサーバー起動

if __name__ == '__main__':
  # サーバを起動
  app.run(debug=True, host='0.0.0.0', port=TM_PORT_NO)

 

appにはFlaskオブジェクトが格納されているのでapp.run()でサーバーを起動。

引数のdebug=Trueではデバッグを出力するようにしている。

host='0.0.0.0.'を指定しないと外のネットワークから接続ができないらしい。パソコンでこのローカルサーバーを立ち上げ、別の端末からこのサーバーにアクセスするために必要。実際に自身のスマホから接続できた。

portはポート番号に指定。

 

ルート(/)にアクセスしたときの処理

@app.route('/', methods=['GET'])
def index():
  with open("index.html", "rb") as f:
  return f.read()

@app.route()でルートパスにアクセスされた時の後述する関数を実行するようにしている。methods=['GET']でGETリクエストを指定。

index()関数では同階層のindex.htmlファイルを読み込み、表示している。

f:id:hanamichi_sukusuku:20210219202655p:plain

http://localhost:8888/このURLにアクセスした時の画像。

 

/apiにアクセスしたときの処理

@app.route('/api', methods=['GET'])
def api():
 # URLパラメータを取得 --- (*3)
 q = request.args.get('q', '')
 if q == '':
  return '{"label": "空です", "per":0}'
 print("q=", q)
 # テキストのジャンル判定を行う --- (*4)
 label, per, no = my_text.check_genre(q)
 # 結果をJSONで出力
 return json.dumps({
   "label": label,
   "per": per,
   "genre_no": no
 })

@app.route()で/apiにアクセスした時、後述する関数を実行するようにしている。ここでもmethods=['GET']でGETリクエストを指定。

request.args.get()ではURLからパラメーターをしている。第一引数の'q'はキー。URLで'q'をキーとするパラメーターを取得し変数qに格納。

 

my_text.check_genre()では事前の作成したmy_text.pyからcheck_genre()関数を使用している。上記のリンクを見ればわかるが、この関数では渡した文章を機械学習で分類し、どのラベルを示すものか、確率、ラベルデータの番号をそれぞれ返す。

return json.dumps()で第一引数に辞書を渡すとjson文字列として出力されたものを返す。

 

一番上の実行結果には

http://localhost:8888/api?q=(テキスト)という形でアクセスしたときの実行結果を表示している。

 

 

 

 

 

文章を指定してTF-IDFに変換しディープラーニングで判定

f:id:hanamichi_sukusuku:20210218141308p:plain

f:id:hanamichi_sukusuku:20210218141335p:plain

実行結果

 

f:id:hanamichi_sukusuku:20210218141433p:plain

このプログラムではMLPを利用して文章を指定しどんなジャンルの文章なのか判定するプログラム。

インポートしているtfidfモジュールは下記のリンクで作成したものを使用する。

TF-IDFの手法でモジュール作成 - hanamichi_sukusukuのブログ

 

独自テキストの定義、TF-IDFの辞書を読み込む

text1 = """
野球を観るのは楽しいものです。
試合だけでなくインタビューも楽しみです。
"""
text2 = """
常にiPhoneiPadを持っているので、
二口あるモバイルバッテリがあると便利。
"""
text3 = """
幸せな結婚の秘訣は何でしょうか。
夫には敬意を、妻には愛情を示すことが大切。
"""

# TF-IDFの辞書を読み込む --- (*2)
tfidf.load_dic("text/genre-tdidf.dic")

""" """は中身の改行をそのままの状態で扱うことができる。改行文字\nで表記した場合と同じ。

 

tfidf.laod_dic()は作成したtfidfモジュールのload_dic()関数である。この関数では引数で渡したパスから保存した単語辞書や単語の出現頻度を格納してあるデータをtfidfモジュール内で読み込み、グローバル変数として使用できるようにするもの。これによりtfidfモジュールから単語辞書、全文章での単語の出現頻度、livedoorニュースコーパスの文章をIDで表現したデータの3つが使用できるようになる。

 

モデル定義

nb_classes = 4
dt_count = len(tfidf.dt_dic)
model = Sequential()
model.add(Dense(512, activation='relu', input_shape=(dt_count,)))
model.add(Dropout(0.2))
model.add(Dense(512, activation='relu'))
model.add(Dropout(0.2))
model.add(Dense(nb_classes, activation='softmax'))
model.compile(
  loss='categorical_crossentropy',
  optimizer=RMSprop(),
  metrics=['accuracy'])
model.load_weights('./text/genre-model.hdf5')

dt_countはMLPの入力レイヤーでの入力数を格納している。tfidf.dt_dicはlivedoorニュースコーパスを使用した4ジャンルの文章全体での単語の出現頻度が格納されているデータ。一つの文章はこのdt_dicにある単語数で表現されるのでこの要素数が入力数になる。具体的には文章中の単語を辞書のidで表現し、その出現頻度と希少性を掛け合わせたTF-IDFのデータを使い、モデルに学習させるので入力数としては辞書の単語数(要素数)を使用する。

 

model.load_weights()で重みデータを読み込んでいる。この重みデータはlivedoorニュースコーパスのテキストを事前にMLPで学習し、その時の重みデータを保存したもの。

 

関数の呼び出し

if __name__ == '__main__':
check_genre(text1)
check_genre(text2)
check_genre(text3)

 

check_genre()関数

def check_genre(text):
 # ラベルの定義
  LABELS = ["スポーツ", "IT", "映画", "ライフ"]
 # TF-IDFのベクトルに変換 -- (*5)
  data = tfidf.calc_text(text) 
 # MLPで予測 --- (*6)
  pre = model.predict(np.array([data]))[0]
  n = pre.argmax()
  print(LABELS[n], "(", pre[n], ")")
  return LABELS[n], float(pre[n]), int(n)

この関数では引数で受け取ったテキストをモデルに学習させ予測結果を出力するためのもの。

tfidf.calc_text()でtfidfモジュール内の単語辞書を更新せずにTF-IDFベクトルに変換する。(単語辞書の要素数で表現されたTF-IDFベクトルデータ)

model.predict()で結果を予測。

argmax()で予測結果から最も値が大きいインデックス番号を返す。

LABELSに定義している要素のインデックスとラベルデータのインデックスは対応しているので予測したラベルと確率を出力。

 

・このプログラムでは学習ずみの単語しかベクトル化できない。今回作成したモジュールでは、livedoorニュースコーパスに出てこない、未知語を見つけると単語をなかったことにする処理にしてあるため学習したことない単語が多く出てくるほど、判定結果が悪くなる。そこで、未知語が出てきたら覚えておいて、改めて学習をやり直すなど、工夫が必要になる。

 

 

 

 

 

 

NaiveBayesでTF-IDFで作成したデータベースを学習

f:id:hanamichi_sukusuku:20210217164325p:plain

実行結果

f:id:hanamichi_sukusuku:20210217164354p:plain

このプログラムではNaiveBayes(ナイーブベイズ)を利用してTF-IDFのデータベースを学習している。

 

TF-IDFのデータベースを読み込む

data = pickle.load(open("text/genre.pickle", "rb"))
y = data[0] # ラベル
x = data[1] # TF-IDF

 

 

hanamichi-sukusuku.hatenablog.com

genre.pickleに関しては上記でlivedoorニュースコーパスのデータを利用してTF-IDFベクトルに変換したデータを作成している。

 

学習用とテスト用に分ける

x_train, x_test, y_train, y_test = train_test_split(
   x, y, test_size=0.2)

 

 

ナイーブベイズで学習

model = GaussianNB()
model.fit(x_train, y_train)

 

評価して結果を出力

y_pred = model.predict(x_test)
acc = metrics.accuracy_score(y_test, y_pred)
rep = metrics.classification_report(y_test, y_pred)
 
print("正解率=", acc)
print(rep)

metrics.classification_report()では正解データと予測データを渡すことでprecision(適合率)やrecall(再現率)などのそれぞれのデータを比較したときの詳細をみることができる。

metrics.classification_report()見方

y_test = [0,0,0,1](正解)

y_pred = [0,0,1,1](予測)

例えばmetrics.classification_report(y_test, y_pred)このような場合

f:id:hanamichi_sukusuku:20210217171023p:plain

precision

 

・precision(適合率)は0と予測した2つはどちらも正解なので適合率1.00。

・1と予測した2つはそのうちの1つが正解なので適合率0.50。

recall

・recall(再現率)は正解が0だった3つのうち、正しく0だと予測されたものは2つなので再現率0.67。

・正解が1だった1つを予測結果で予測できているので再現率1.00。

f1-score

fi-scoreは調和平均。

 

support

正解の要素数

 

文章をTF-IDFのデータベースに変換

このプログラムではlivedoorニュースコーパスを利用する。

スポーツ、IT、映画、ライフの4つに分けたデータベースを生成する。

f:id:hanamichi_sukusuku:20210216185539p:plain

実行結果

genre.pickleというファイルにTF-IDFに文章を変換したデータを格納し保存する。

tfidfモジュール内で生成した単語辞書、テキストをIDで表現したデータ、全文章での単語の出現頻度のデータも保存。

 

ファイル読み込み

y =
x =

# ディレクトリ内のファイル一覧を処理 --- (*1)
def read_files(path, label):
print("read_files=", path)
files = glob.glob(path + "/*.txt")
for f in files:
if os.path.basename(f) == 'LICENSE.txt': continue
tfidf.add_file(f)
y.append(label)

# ファイル一覧を読む --- (*2)
read_files('text/sports-watch', 0)
read_files('text/it-life-hack', 1)
read_files('text/movie-enter', 2)
read_files('text/dokujo-tsushin', 3)

read_files()関数に事前にlivedoorニュースコーパスからtextディレクトリに作成しておいたそれぞれのファイルのパスとラベルにする値を渡す。

glob.glob()でファイルの中身のテキストファイル名を全て取得。

os.path.basename()では引数に渡したパスからファイル名を取得してくれる。LICENSE.txtの場合はcontinue。

tfidf.add_file()は自作したモジュールから関数を使用している。

hanamichi-sukusuku.hatenablog.com

上記のリンクでモジュールを作成している。

tfidf.add_file()

def add_file(path):
  '''テキストファイルを学習用に追加する''' # --- (*6)
  with open(path, "r", encoding="utf-8") as f:
   s = f.read()
   add_text(s)

引数で受け取ったパスのファイルを読み取り用で開き、read()で読み込む。

add_text()ではテキストをIDリストに変換してくれる。

tfidf.add_file()関数を実行すると単語辞書、テキストをIDで表記したデータを生成してくれる。

モジュールの詳細は上記リンク。

y.append()でラベルデータ作成。

 

TF-IDFベクトルデータをxに格納

x = tfidf.calc_files()

tfidf.calc_files()ではTF-IDFの手法で文章中に出現した単語の出現頻度を重要も考慮した形でのデータに変換し呼び出し元に返す。同時に関数で全テキストデータでの単語の出現頻度のデータも生成している。

このxの中身を確認すると

f:id:hanamichi_sukusuku:20210216192046p:plain

TF-IDFでの単語の重要度を考慮した形でのデータになっている。

 

データの保存

pickle.dump([y, x], open('text/genre.pickle', 'wb'))
tfidf.save_dic('text/genre-tdidf.dic')
print('ok')

pickle.dump()でラベルデータ、TF-IDFベクトルに変換したテキストデータをtextディレクトリにgenre.pickleという名前のファイルを作成し保存。

tfidf.save_dic()では引数に指定したパスに単語辞書、テキストをIDで表現したデータ、全文章での単語の出現頻度のデータ(一つのファイルで複数回同じ単語が出現しても足し合わせない。一度でも複数でもその単語をキーとする値は1)を保存する。