MMVCを試してみる(Google Colaboratory版)

以前、WSL2にて試したMMVCをGoogle Colaboratoryで動かしてみる。

MMVC_Trainerを手順通りに動かす

非常にわかりやすいColab用の実行ファイルがあるので、まずはそれで試してみる。手順通りに実行するだけで、特に環境設定は変更する必要はなかった。

00_Rec_Voice.ipynbのrec関数の下記1点だけ修正した。

-  librosa.display.waveplot(speecht, sr=rate)
+  librosa.display.waveshow(speecht, sr=rate)

録音中にマイクが反応しなくなることがあったが、Webページを再読み込みすると問題なく先に進めることが出来た。これは自分のPCの問題だと思われる。

次に、順番通り01_Create_Configfile.ipynbを実行する。

その後、02_Train_MMVC.ipynbを実行してトレーニングを実施するのだが、無料会員の場合は途中で利用上限により「GPU バックエンドに接続できません」となってしまう。

その場合は、ユニットを購入するかしばらく待って(1日以上?)から、「6 学習を実行する」の「New_or_Resume:」をResumeに切り替えてから再度実行する。

最終的に「7 学習結果をONNXファイルに変換」まで問題なく実施することが出来た。

MMVC_Clientをローカル環境で動かす

Windowsの実行ファイルを利用する。公式からダウンロードしたファイルを任意のフォルダに解凍する。

環境設定myprofile.confの入出力デバイスの部分を書き換える。他はデフォルトで良いと思う。

MMVC_Trainerで作成した学習済みファイル(config.json、G_latest_99999999.onnx)をlogsフォルダの下に20220306_24000フォルダを作成してコピーする。

最後に、mmvc_client.batを実行する。

Stable Diffusion web UIをインストールして絵を描く

Stable Diffusion web UIをWindows 11のローカル環境で実行するための、覚書。

目次

環境

  • Windows 11
  • GeForce 10以上のGPUがインストールされている

事前準備

  • Pythonをインストールする
  • Gitをインストールする

Stable Diffusion web UI

  • ダウンロード
  • インストールと実行
  • 画像の出力

Pythonをインストールする

Windows 11を使用しているなら、Microsoft Storeからインストールする。Pythonで検索して、バージョンは3.10を選択する。

コマンドプロンプトで下記のコマンドを実行して、Pythonの後にバージョンが表示されればインストール成功。

> python --version
Python 3.10.10

Gitをインストールする

Git for Windowsのサイトからインストーラーをダウンロードして実行する。

コマンドプロンプトで下記のコマンドを実行して、gitの後にバージョンが表示されればインストール成功。

> git --version
git version 2.39.2.windows.1

Stable Diffusion web UIのダウンロード

インストールしたいフォルダで下記のコマンドを実行する。
git clone https://github.com/AUTOMATIC1111/stable-diffusion-webui.git

> git clone https://github.com/AUTOMATIC1111/stable-diffusion-webui.git
Cloning into 'stable-diffusion-webui'...
remote: Enumerating objects: 17046, done.
remote: Counting objects: 100% (253/253), done.
remote: Compressing objects: 100% (173/173), done.
remote: Total 17046 (delta 148), reused 151 (delta 79), pack-reused 16793
Receiving objects: 100% (17046/17046), 27.92 MiB | 7.52 MiB/s, done.
Resolving deltas: 100% (11888/11888), done.

modelのダウンロード

Stable Diffusion web UIだけでは画像を出力することは出来ない。様々な画像を学習させて作成した「model」が必要になる。「model」の種類によってアニメ系が得意、リアル系が得意、背景が得意など色々あるが、今回は下記のアニメ系が得意なモデルをダウンロードする。画像を出力して気に入らなければ、別のモデルを探せば良い。

https://huggingface.co/andite/anything-v4.0/tree/mainのページにあるanything-v4.0-pruned.safetensors(v4.5の方が良いかも)をダウンロードして、Stable Diffusion web UIのインストールフォルダ下のmodels/Stable-diffusionフォルダに保存する。

VAEのダウンロード

VAE(変分自己符号化器)の導入は必須ではない。
必須ではないが、VAEを導入することで画像が鮮明になったり、より細かいディテールで出力されるようになる。

https://huggingface.co/stabilityai/sd-vae-ft-mse-original/tree/mainのページにあるvae-ft-mse-840000-ema-pruned.safetensorsをダウンロードして、Stable Diffusion web UIのインストールフォルダ下のmodels/VAEフォルダに保存する。

この先の手順を実行して、Stable Diffusion web UIをインストールした後に、Stable Diffusion web UIの画面上で下記の設定を行う。

画面上のメニューの「Settings」、次に左のメニューの「Stable Diffusion」をクリックし、SD VAEの項目のプルダウンからvae-ft-mse-840000-ema-pruned.safetensorsを選択し、「Apply settiongs」をクリックする。

EasyNegativeのダウンロード

Stable Diffusion web UIでは、Promptに入力したテキストによって画像の出力内容を制御する。しかし、それだけでは低品質の画像が出力されたり、足が3本あったり、指の方向がおかしい画像が出力されたりする。

そこで画像を出力するときは、Negative Promptに「(worst quality, low quality:1.4), multiple limbs」のようなお決まりの文言を入力してそれらの画像が出力されることを抑制する。EasyNegativeを使用するとそれらの面倒な入力をある程度省略することが出来るようになる。

https://huggingface.co/datasets/gsdf/EasyNegative/tree/mainのページにあるEasyNegative.safetensorsをダウンロードして、Stable Diffusion web UIのインストールフォルダ下のembeddingsフォルダに保存する。

ダウンロード後、Stable Diffusion web UIが起動中であれば再起動する。Negative PromptにEasyNegativeと入力すれば適用出来る。

Stable Diffusion web UIのインストールと実行

インストール前にダウンロードしたフォルダのwebui-user.batをエディターで開き、COMMANDLINE_ARGS=の後に–xformersを追記する。

@echo off

set PYTHON=
set GIT=
set VENV_DIR=
set COMMANDLINE_ARGS=--xformers

call webui.bat

webui-user.batを実行する。初回起動時、実行に必要なソフトウェアが自動的にダウンロードされる。データ通信量(数GBほど)も多くかなり時間がかかる。

色々メッセージが表示されるが、下記の一文が表示さればインストールと起動が成功している。

Running on local URL:  http://127.0.0.1:7860

上記のURLにブラウザからアクセスすると、下記の画面が表示される。

画像の出力

txt2imgタブのPromptに文言を入力し、Generateをクリックすると、画像が出力できる。しかし、最初は何を入力したら良いかわからないと思う。

そこで、PNG Infoタブの機能を使用する。このSource欄にStable Diffusion web UIによって出力された画像をドラッグアンドドロップすると、その画像を出力したときのPromptを見ることが出来る。Promptが表示されたらSend to txt2imgをクリックすることで内容をコピーすることが出来る。

例えば、AIによって出力されたと表記されている画像を画像投稿サイトからダウンロードして、上記の機能を利用すれば同じような画像を出力出来る。

ただし、使用している「model」が違えば生成される画像はかなり違ってくるため、出来るだけ同じ「model」で出力された画像を選ぶか、同じ「model」をインストールしておく。どの「model」を使用しているかも、PNG Infoで確認出来る。

試しに下記画像をダウンロードして、画像を生成してみる。

上記の画像をダウンロードしたら、PNG InfoのSource欄にドラッグアンドドロップする。すると下記のように出力時の情報が表示されるので、Send to txt2imgをクリックする。

txt2imgに先ほどの内容が入力されたのを確認したら、Generateをクリックする。(初回実行時は時間がかかる)

ほぼ同じ画像が出力された。Seed欄横のサイコロのようなボタンをクリックすれば、Seed値が初期化されランダム生成に変わるので、Promptの条件に沿った違う画像を出力することが出来る。

その他設定変更

Clip skipには2を設定すると良いらしい。

Stable DiffusionをWSL上のUbuntu22.04で動かす

環境

Windows 11
WLS2
Ubuntu 22.04
Python 3.10.4

Hugging Faceのアカウント作成

Hugging Faceのアカウント作成し、Tokenを取得する。ユーザーの設定画面のAccess Tokenを開き、New tokenをクリックしてTokenを作成する。後で使用するためコピーしておく。※Email認証がまだの場合はボタンが非活性になっているので注意

作成時の名前は適当で良い。

インストール

pip install diffusers==0.2.4 transformers scipy ftfy

実行

下記の通り実行し、Tokenを入力する。

$ huggingface-cli login

        _|    _|  _|    _|    _|_|_|    _|_|_|  _|_|_|  _|      _|    _|_|_|      _|_|_|_|    _|_|      _|_|_|  _|_|_|_|
        _|    _|  _|    _|  _|        _|          _|    _|_|    _|  _|            _|        _|    _|  _|        _|
        _|_|_|_|  _|    _|  _|  _|_|  _|  _|_|    _|    _|  _|  _|  _|  _|_|      _|_|_|    _|_|_|_|  _|        _|_|_|
        _|    _|  _|    _|  _|    _|  _|    _|    _|    _|    _|_|  _|    _|      _|        _|    _|  _|        _|
        _|    _|    _|_|      _|_|_|    _|_|_|  _|_|_|  _|      _|    _|_|_|      _|        _|    _|    _|_|_|  _|_|_|_|

        To login, `huggingface_hub` now requires a token generated from https://huggingface.co/settings/tokens .
        
Token: 
Login successful
Your token has been saved to /home/ユーザーID/.huggingface/token

Hugging Faceにログインし、以下のページにアクセスして、Access repositoryをクリックする。
https://huggingface.co/CompVis/stable-diffusion-v1-4

cudaの設定が面倒なので、今回はcudaなしで実行する。

from diffusers import StableDiffusionPipeline

pipe = StableDiffusionPipeline.from_pretrained("CompVis/stable-diffusion-v1-4", use_auth_token=True)

prompt = "a photograph of an astronaut riding a horse"

image = pipe(prompt)["sample"][0]

image.save("horse.png")

image

実行結果

MMVCを試してみる

MMVCというAIを使ったリアルタイムボイスチェンジャーなるものを見つけた。自分の声を収録して、それを別人の声に変換するとは、映画「ミッションインポッシブル3」で見たようなシーン(厳密には少し違うが)が実際に実行可能になるなんて、とても面白そうだったので試してみたくなった。

環境

公式ではgoogle colaboratoryを使っていて、そのまま何も変更することなく動かすことが出来る。しかし、自前のPCで実行したくなった。Windows WSL2環境で実行するのは結構面倒なのでおすすめはしません。

Windows 11
WLS2
Ubuntu 20.04
Python 3.8.10
cuda-toolkit-11-6 is already the newest version (11.6.2-1).
libnccl-dev is already the newest version (2.12.10-1+cuda11.6).
libcudnn8 is already the newest version (8.4.0.27-1+cuda11.6).
VSCODE 1.66.2

MMVCのライセンス情報

MMVCv1.2.0.2
Copyright (c) 2021 Isle.Tennos 
Released under the MIT license 
https://opensource.org/licenses/mit-license.php
git:https://github.com/isletennos/MMVC_Trainer
community(discord):https://discord.gg/PgspuDSTEc

MMVC_Trainerのインストール

公式からレポジトリをダウンロードして、VSCODEの作業フォルダに解凍する。

MMVC_Trainerの実行

自身の音声ファイルの準備

公式のページに書かれている通り、自分の音声を収録していく。(100文読み上げるのは結構大変だった)

インストールと環境設定

以下の通りインストールする。CUDAのインストールについてはこちらを参照のこと。

sudo apt install cmake
sudo apt install espeak

pip install --upgrade torch torchvision torchaudio --extra-index-url https://download.pytorch.org/whl/cu113
pip install pyopenjtalk

現時点での最新版をインストールしている。

cmake is already the newest version (3.16.3-1ubuntu1).
espeak is already the newest version (1.48.04+dfsg-8build1).
torch in ./.local/lib/python3.8/site-packages (1.11.0+cu113)
torchvision in ./.local/lib/python3.8/site-packages (0.12.0+cu113)
torchaudio in ./.local/lib/python3.8/site-packages (0.11.0+cu113)
pyopenjtalk in ./.local/lib/python3.8/site-packages (0.2.0)

Create_Configfile.ipynbの実行

Create_Configfile.ipynbを実行して、環境ファイルを作成する。Googleドライブ、インストール部分の実行は不要。フォルダの指定はローカルの環境に合わせる。

Train_MMVC.ipynbの実行

モデルをトレーニングしていく。自前の環境には以下のものがインストールされている。本来は公式から指定されているバージョンをインストールするべき。下記のように違うバージョンを使う場合は自己責任で。

Cython in ./.local/lib/python3.8/site-packages (0.29.28)
librosa in ./.local/lib/python3.8/site-packages (0.9.1)
matplotlib in ./.local/lib/python3.8/site-packages (3.5.2)
numpy in ./.local/lib/python3.8/site-packages (1.21.6)
phonemizer in ./.local/lib/python3.8/site-packages (3.1.1)
scipy in ./.local/lib/python3.8/site-packages (1.8.0)
tensorboard in ./.local/lib/python3.8/site-packages (2.8.0)
Unidecode in ./.local/lib/python3.8/site-packages (1.3.4)
retry in ./.local/lib/python3.8/site-packages (0.9.2)
tqdm in ./.local/lib/python3.8/site-packages (4.64.0)

./monotonic_align/setup.pyを下記の通り実行して、core.cpython-38-x86_64-linux-gnu.soをビルドする。

python3 setup.py build_ext --inplace

train_ms.pyを以下の通り、修正する。

import warnings
warnings.simplefilter('ignore', FutureWarning)
warnings.simplefilter('ignore', UserWarning)
train_ms.py:50行目
  os.environ['MASTER_PORT'] = '8000'

train_config_zundamon.jsonを以下の通り修正する。自前のPCではOut of Memoryになってしまったため、batch_sizeを5に変更した。

"batch_size": 5,

後はTrain_MMVC.ipynbファイルの指示通り実行していく。Googleドライブ、インストール部分の実行は不要。フォルダの指定はローカルの環境に合わせる。実行に成功すれば下記のようにモデルのトレーニングが進んでいく。

Epoch 436:  90%|████████████████████████████▋   | 70/78 [00:51<00:05,  1.49it/s]

すべて実行し終わるにはかなり時間がかかると思うので、自分は適当なところで中断して、トレーニング途中のモデルを使用してVC処理を動かすことにした。

学習したモデルの検証

MMVC_Interface.ipynbファイルの指示通り実行して自分の音声が変換されることを確認する。学習が足りないとうまく変換されないかもしれないが、自分が実行したことろではそれなりに満足のいく結果だったので、次のリアルタイム変換に進むことにした。

※VSCODEだとIPython.display.Audioは使えないので、wavファイルをして書き出すように変更しておく。

ipd.display(ipd.Audio(audio1, rate=hps.data.sampling_rate))
write('converted.wav', hps.data.sampling_rate, audio1)

MMVC_Clientのインストール

公式からレポジトリをダウンロードして、VSCODEの作業フォルダに解凍する。

※2022/05/11追記 WSL2環境からマイクを使用することがまだ出来なくて未検証。出来るようになり次第追記予定。

※2023/04/01追記 MMVC_client v0.3.1.0が出て、簡単にクライアントを使用できるようになった。こちらでも解説している。

pulseaudioのインストールと実行

Windows側

default config filesフォルダにあるdefault.paファイルをコピーして、以下行を変更する。

load-module module-waveout sink_name=output source_name=input record=0
load-module module-native-protocol-tcp auth-ip-acl=127.0.01;10.0.0.0/8;172.16.0.0/12;192.168.0.0/16
E:\pulse\pulseaudio.exe -F E:\pulse\default.pa --exit-idle-time=600

初回起動時、外部からのアクセスを許可するか問われるので、「パブリックネットワーク」にチェックを入れてアクセスを許可する。

Ubuntu側
sudo apt install portaudio19-dev
pip install pyaudio
pip install noisereduce
echo 'export PULSE_SERVER=tcp:$(grep nameserver /etc/resolv.conf | awk '\''{print $2}'\'')' >> ~/.profile
source ~/.profile

output_audio_device_list.pyの実行

rec_environmental_noise.pyの実行

mmvc_client_GPU.pyの実行

  - ext_modules = cythonize("d:/code/monotonic_align/core.pyx"),
  + ext_modules = cythonize("core.pyx"),

Recommendation Systemを考える その4

Recommendation Systemを考える その3の続き。

1.やりたいこと

その3でやりたかったことの続き。回帰ではなく分類で予測してみる。

2.分類

データを読み込む

前回作成したDoc2Vecデータを読み込み、配列に変換する。

from gensim.models.doc2vec import Doc2Vec
from pathlib import Path

MODEL_DATA_PATH = str(Path.home()) + '/vscode/syosetu/data/search_novel_doc2vec_100.model'

m = Doc2Vec.load(MODEL_DATA_PATH)
vectors_list = [m.dv[n] for n in range(len(m.dv))]

データ処理しやすいように、必要な情報をPandasにまとめる。

import pandas as pd
import numpy as np

MODEL_TITLES_CSV_PATH = str(Path.home()) + '/vscode/syosetu/data/search_novel_titles.csv'

df = pd.read_csv(MODEL_TITLES_CSV_PATH, thousands=',')
df = df.drop(columns=['URL'])
df['VECTORS'] = vectors_list
データを加工する

評価ポイントから 不要な文字を取り除き、数値に変換する。

df['EVALUATION'] = df['EVALUATION'].str.replace(',', '')
df['EVALUATION'] = df['EVALUATION'].str.replace('評価受付停止中', '').astype(float)

df['EVALUATION'].describe()

count      2000.000000
mean      32735.061500
std       33540.729876
min        4019.000000
25%       14118.000000
50%       21271.000000
75%       38219.250000
max      323929.000000
Name: EVALUATION, dtype: float64

評価ポイントの値によって5つくらいに分類する。

df['EVALUATION_BAND'] = pd.cut(df['EVALUATION'], 5)
df[['EVALUATION_BAND', 'EVALUATION']].groupby(['EVALUATION_BAND'], as_index=False).count().sort_values(by='EVALUATION_BAND', ascending=True)

EVALUATION_BAND				EVALUATION
0	(3699.09,   68001.0]	1801
1	(68001.0,  131983.0]	149
2	(131983.0, 195965.0]	38
3	(195965.0, 259947.0]	6
4	(259947.0, 323929.0]	6

3699.09から68001.0が最も多く1801件も存在する。逆に予測したい評価の高い小説は6件しか存在しない。今回予測したいのは数の少ない小説の方なので、このままでは正しく予測することが出来ない。そこで区分2、3、4を一つのグループ、区分1のグループ、区分0のグループに分けることにする。

df.loc[ df['EVALUATION'] <= 68001, 'EVALUATION'] = 0
df.loc[(df['EVALUATION'] >  68001) & (df['EVALUATION'] <= 131983), 'EVALUATION'] = 1
df.loc[ df['EVALUATION'] > 131983, 'EVALUATION'] = 2

df = df.drop(['EVALUATION_BAND'], axis=1)
df['EVALUATION'].value_counts()

0.0    1801
1.0     149
2.0      50
Name: EVALUATION, dtype: int64

区分2の50本に合わせて他の区分からランダムに50本の小説を抽出することにする。

df_0 = df[df['EVALUATION'] == 0].sample(n=50, random_state=0)
df_1 = df[df['EVALUATION'] == 1].sample(n=50, random_state=0)
df_2 = df[df['EVALUATION'] == 2]

df = pd.concat([df_0, df_1, df_2])
作成したデータを元に予測する

先程作成したデータをトレーニングデータとテストデータに分ける。

from sklearn.model_selection import train_test_split

X = pd.DataFrame(df['VECTORS'].tolist(), index=df.index)
X.columns = [f'No{i+1}' for i in range(len(X.columns))]
y = df['EVALUATION']

X_train, X_test, y_train, y_test = train_test_split(X, y, random_state=0)

試しにランダムフォレストにかけてみる。

from sklearn.ensemble import RandomForestClassifier

random_forest = RandomForestClassifier(random_state=0)
random_forest.fit(X_train, y_train)

random_forest.score(X_test, y_test)

0.5263157894736842
random_forest.predict(X_test)

array([2., 1., 2., 2., 2., 1., 0., 1., 1., 0., 2., 1., 2., 2., 1., 0., 1.,
       1., 2., 1., 0., 2., 2., 0., 0., 1., 2., 1., 1., 0., 2., 2., 1., 2.,
       2., 2., 2., 1.])

5割を少しだけ超えるほどの精度での予測になった。このままチューニングしても良い結果は得られそうにない。トレーニングデータの作成からやり直す必要がありそう。

3.まとめ

評価ポイントを文章のみから予測するのは難しそう。

Recommendation Systemを考える その3

Recommendation Systemを考える2の続き。

1.やりたいこと

投稿されて間もない小説はユーザーによる評価がされていない場合が多いため、初期の評価ポイントはその小説の本来のおもしろさを反映していないと思われる。
小説の内容を機械学習で評価して、今後付くであろう評価ポイントを予測する。
最終的には新規投稿された小説、もしくは評価受付設定offになっている小説から評価ポイントが高くなると予想される小説を抽出するプログラムを作成する。

2.小説のスクレイピング

これまでの方法は累積ランキングから小説をダウンロードするため、Top300までしか小説を参照出来なかった。そのため、常にデータが不足していた。今回は方法を変えて小説検索画面から小説を参照するためのURL一覧を作成する。

まずは「小説を読もう!」の検索画面のURLを生成し、検索結果一覧を取得する。検索結果一覧は1ページあたり20リンクあり、最大100ページまで遡って表示する事ができる。その取得した結果をテキストファイルに書き出しておく。

import requests
import socket

from bs4 import BeautifulSoup
from pathlib import Path
from urllib.error import HTTPError, URLError
from time import sleep
from tqdm import tqdm

REQUEST_HEADERS = {
    'User-Agent':
    'Mozilla/5.0 (Windows NT 10.0; WOW64; Trident/7.0; rv:11.0) like Gecko'
  }
REQUEST_TIMEOUT = 20

SEARCH_URL = 'https://yomou.syosetu.com/search.php'
NOVEL_SEARCH_RESULT_PATH = str(Path.home()) + '/vscode/syosetu/data/search_novels.txt'

# 小説を検索してダウンロード
def novel_search_dler(max_search_page=100, max_search_result=20):
  with open(NOVEL_SEARCH_RESULT_PATH, 'w') as f:
    for page in tqdm(range(max_search_page)):
      #search_params = {'word': '', 'type': 're', 'order_former': 'search', 'order': 'new', 'notnizi': '1', 'minlen': '5000', 'min_globalpoint': '1000', 'p': page + 1}
      search_params = {'word': '', 'type': 're', 'order_former': 'search', 'order': 'hyoka', 'notnizi': '1', 'minlen': '5000', 'min_globalpoint': '1000', 'p': page + 1}
      r = requests.get(SEARCH_URL, headers=REQUEST_HEADERS, timeout=REQUEST_TIMEOUT, params=search_params)
      r.encoding = r.apparent_encoding
      soup = BeautifulSoup(r.text, 'html.parser')
      sleep(1)

      search_result = soup.find_all('div', class_='searchkekka_box')

      for r_count in range(max_search_result):
        a_list = search_result[r_count].find_all('a', limit=3)
        url = a_list[0].get('href')
        info_url = a_list[2].get('href')
        title = a_list[0].get_text()
        # 小説のURL、小説情報のURLと小説のタイトルを設定
        f.write('{0}\t{1}\t{2}\n'.format(url, info_url, title))
        print('url: {0} info_url: {1} title: {2}'.format(url, info_url, title))

novel_search_dler()

続いて先程作成した検索結果を順番に読み込み、小説の内容と評価ポイントを取得する。※morphologicalAnalysisのコードはmecab設定のページに記載。

import morphologicalAnalysis as ma

import requests
import socket

import pandas as pd

from bs4 import BeautifulSoup
from pathlib import Path
from urllib.error import HTTPError, URLError
from time import sleep
from tqdm import tqdm

REQUEST_HEADERS = {
    'User-Agent':
    'Mozilla/5.0 (Windows NT 10.0; WOW64; Trident/7.0; rv:11.0) like Gecko'
  }
REQUEST_TIMEOUT = 60

SCRAP_WORD = 5000
SCRAP_CHAPTER = 40

COL_NAMES = ['URL', 'INFO_URL', 'TITLE']
NOVEL_SEARCH_RESULT_PATH = str(Path.home()) + '/vscode/syosetu/data/search_novels.txt'
NOVEL_SEARCH_RESULT_DATA_PATH = str(Path.home()) + '/vscode/syosetu/data/search_novel_datas.txt'

# 小説情報をダウンロード
def novel_info_dler(url):
  KEYWORD = 2
  EVALUATION = 6

  r = requests.get(url, headers=REQUEST_HEADERS, timeout=REQUEST_TIMEOUT)
  r.encoding = r.apparent_encoding
  soup = BeautifulSoup(r.text, 'html.parser')
  sleep(1)

  keyword = soup.find(id='noveltable1').find_all('td')[KEYWORD].get_text(strip=True)
  evaluation = soup.find(id='noveltable2').find_all('td')[EVALUATION].get_text(strip=True).replace('pt', '')
  return keyword,evaluation

# 本文をダウンロード
def novel_text_dler(url):
  r = requests.get(url, headers=REQUEST_HEADERS, timeout=REQUEST_TIMEOUT)
  r.encoding = r.apparent_encoding
  soup = BeautifulSoup(r.text, 'html.parser')
  sleep(1)

  novel_text = ''
  # 章のタイトルに「設定」、「登場人物」が含まれる場合、戻り値は''とする
  subtitle = soup.find('p', class_='novel_subtitle')
  if subtitle is None or '設定' in subtitle or '登場人物' in subtitle:
    return novel_text

  honbun = soup.find_all('div', class_='novel_view')

  for text in honbun:
    novel_text += text.text.replace('\n', ' ') # 改行コードをスペースに変換
  return novel_text

# 小説の各話をダウンロード
def novel_chapter_dler(url, scrap_word=SCRAP_WORD, scrap_chapter=SCRAP_CHAPTER):
  chapter = 0
  word_count = 0
  total_words = ''

  while word_count < scrap_word and chapter < scrap_chapter:
    # 単語がscrap_word未満かつ、scrap_chapter話以下の間繰り返し取得する
    try:
      words = novel_text_dler('{0}{1}/'.format(url, chapter + 1))
    except (HTTPError, URLError) as e:
      print(e)
      break
    except socket.timeout as e:
      print(e)
      continue
    else:
      #print('chapter:{0}'.format(chapter + 1))
      word_count += len(ma.analysis(words).split())
      total_words += words
    chapter += 1
  return total_words

# 小説の検索結果をもとにダウンロード
def novel_search_data_dler():
  df = pd.read_table(NOVEL_SEARCH_RESULT_PATH, header=None, names=COL_NAMES)

  with open(NOVEL_SEARCH_RESULT_DATA_PATH, 'w') as f:
    for index,item in tqdm(df.iterrows()):
      url = item[COL_NAMES[0]]
      info_url = item[COL_NAMES[1]]
      title = item[COL_NAMES[2]]
      keyword,evaluation = novel_info_dler(info_url)
      # URL、小説のタイトル、キーワードと評価ポイントを設定
      f.write('{0}\t{1}\t{2}\t{3}\t'.format(url, title, keyword, evaluation))
      #print('url:{0} title:{1} keywords:{2} evaluation:{3}'.format(url, title, keyword, evaluation))
      # 小説の各話を設定
      f.write(novel_chapter_dler(url))
      f.write('\n')

novel_search_data_dler()

サーバーに負荷をかけないようにするため、ページの取得に1秒のインターバルをおいているため、全ての取得には7時間ほどかかる。

3.回帰分析

良い結果は得られていないが、せっかくなので回帰分析するところまでのソースコードを公開する。評価ポイントを5段階くらいに分けて分類したほうが良い結果になると思う。

データを読み込む

Doc2Vecデータを読み込み、配列に変換する。

from gensim.models.doc2vec import Doc2Vec
from pathlib import Path

MODEL_DATA_PATH = str(Path.home()) + '/vscode/syosetu/data/search_novel_doc2vec_100.model'

m = Doc2Vec.load(MODEL_DATA_PATH)
vectors_list = [m.dv[n] for n in range(len(m.dv))]

データ処理しやすいように、必要な情報をPandasにまとめる。

import pandas as pd
import numpy as np

MODEL_TITLES_CSV_PATH = str(Path.home()) + '/vscode/syosetu/data/search_novel_titles.csv'

df = pd.read_csv(MODEL_TITLES_CSV_PATH, thousands=',')
df = df.drop(columns=['URL'])
df['VECTORS'] = vectors_list

Doc2Vecのデータをそのまま使うと次元数が大きすぎるので、PCAを使って次元圧縮する。

from sklearn.decomposition import PCA

def dimension_reduction(data, pca_dimension=20):
  pca_data = data.copy()
  pca = PCA(n_components=pca_dimension)
  vector = np.array([np.array(v) for v in pca_data['VECTORS']])
  pca_vectors = pca.fit_transform(vector)
  pca_data['PCA_VECTORS'] = [v for v in pca_vectors]

  return pca_data

df = dimension_reduction(data=df)
df.info()

<class 'pandas.core.frame.DataFrame'>
RangeIndex: 2000 entries, 0 to 1999
Data columns (total 5 columns):
 #   Column       Non-Null Count  Dtype 
---  ------       --------------  ----- 
 0   TITLE        2000 non-null   object
 1   KEYWORDS     2000 non-null   object
 2   EVALUATION   2000 non-null   object
 3   VECTORS      2000 non-null   object
 4   PCA_VECTORS  2000 non-null   object
dtypes: object(5)
memory usage: 78.2+ KB

不要な文字を取り除き、数値に変換する。

df['EVALUATION'] = df['EVALUATION'].str.replace(',', '')
df['EVALUATION'] = df['EVALUATION'].str.replace('評価受付停止中', '').astype(float)

df['EVALUATION'].describe()

count      2000.000000
mean      32735.061500
std       33540.729876
min        4019.000000
25%       14118.000000
50%       21271.000000
75%       38219.250000
max      323929.000000
Name: EVALUATION, dtype: float64

トレーニングデータとテストデータに分ける。

from sklearn.model_selection import train_test_split

X = pd.DataFrame(df['PCA_VECTORS'].tolist(), index=df.index)
X.columns = [f'No{i+1}' for i in range(len(X.columns))]
y = df['EVALUATION']

X_train, X_test, y_train, y_test = train_test_split(X, y, random_state=0)
データを確認する

必要なライブラリをインポートしておく。

from scipy import stats
from scipy.stats import norm, skew # for some statistics

# visualization
import matplotlib.pyplot as plt
%matplotlib inline

import seaborn as sns
color = sns.color_palette()
sns.set_style('darkgrid')

import warnings
def ignore_warn(*args, **kwargs):
  pass
warnings.warn = ignore_warn # Ignore annoying warning (from sklearn and seaborn)

評価ポイントの分布を可視化する。

sns.distplot(y_train, fit=norm)

# Get the fitted parameters used by the function
(mu, sigma) = norm.fit(y_train)
print( '\n mu = {:.2f} and sigma = {:.2f}\n'.format(mu, sigma))

# Now plot the distribution
plt.legend(['Normal dist. ($\mu=$ {:.2f} and $\sigma=$ {:.2f} )'.format(mu, sigma)],
            loc='best')
plt.ylabel('Frequency')
plt.title('Evaluation distribution')

# Get also the QQ-plot
fig = plt.figure()
res = stats.probplot(y_train, plot=plt)
plt.show()

だいぶ歪んだ分布をしているので、対数変換を行う。

# We use the numpy fuction log1p which applies log(1+x) to all elements of the column
y_train = np.log1p(y_train)

# Check the new distribution 
sns.distplot(y_train, fit=norm)

# Get the fitted parameters used by the function
(mu, sigma) = norm.fit(y_train)
print( '\n mu = {:.2f} and sigma = {:.2f}\n'.format(mu, sigma))

# Now plot the distribution
plt.legend(['Normal dist. ($\mu=$ {:.2f} and $\sigma=$ {:.2f} )'.format(mu, sigma)],
            loc='best')
plt.ylabel('Frequency')
plt.title('Evaluation distribution')

# Get also the QQ-plot
fig = plt.figure()
res = stats.probplot(y_train, plot=plt)
plt.show()
作成したデータを元に予測する

Lassoで回帰分析を実施する。

from sklearn.model_selection import cross_val_score, KFold

kf = KFold(5, shuffle=True, random_state=0).get_n_splits(X_train)

# Validation function
def rmsle_cv(classifier):
  return np.sqrt(-cross_val_score(classifier, X_train.values, y_train.values, scoring="neg_mean_squared_error", cv=kf))
from sklearn.linear_model import Lasso
from sklearn.pipeline import make_pipeline
from sklearn.preprocessing import RobustScaler

lasso = make_pipeline(RobustScaler(), Lasso(random_state=0))

score = rmsle_cv(lasso)
print("Lasso score: {:.4f} ({:.4f})\n".format(score.mean(), score.std()))

Lasso score: 0.7408 (0.0095)

4.まとめ

今回はやりたいことが出来なかったため、前述した通り分類問題で改めて分析してみる。

複数の機械学習アルゴリズムを比較する

Recommendation Systemを考える その2で、機械学習を実施した結果、ロジスティック回帰が良いスコアになったが、その他の分類器ではどうか大まかに調べてみる。

1.複数のアルゴリズムを比較する

ライブラリをインポートする。

# data analysis and wrangling
import pandas as pd
import numpy as np
import random as rnd

# visualization
import matplotlib.pyplot as plt
%matplotlib inline

import seaborn as sns
color = sns.color_palette()
sns.set_style('darkgrid')

import warnings
def ignore_warn(*args, **kwargs):
  pass
warnings.warn = ignore_warn # Ignore annoying warning (from sklearn and seaborn)

# machine learning
from sklearn.linear_model import LogisticRegression
from sklearn.svm import SVC, LinearSVC
from sklearn.ensemble import RandomForestClassifier, AdaBoostClassifier, GradientBoostingClassifier, ExtraTreesClassifier, VotingClassifier
from sklearn.neighbors import KNeighborsClassifier
from sklearn.naive_bayes import GaussianNB
from sklearn.linear_model import Perceptron, SGDClassifier
from sklearn.tree import DecisionTreeClassifier

from sklearn.model_selection import GridSearchCV, cross_val_score, StratifiedKFold, learning_curve

保存しておいたデータを読み込む。

from pathlib import Path

TRAIN_DATA_PATH = str(Path.home()) + '/vscode/syosetu/train_data.pkl'

train = pd.read_pickle(TRAIN_DATA_PATH)

トレーニングデータとテストデータに分割する。

from sklearn.model_selection import train_test_split

X = train['vectors']
y = train['female']

X_array = np.array([np.array(v) for v in X])
y_array = np.array([i for i in y])

X_train, X_test, y_train, y_test = train_test_split(X_array, y_array, random_state=0)

クロスバリデーションのスコアが良い順に表示する。

kfold = StratifiedKFold(n_splits=10)
cls_weight = (y_train.shape[0] - np.sum(y_train)) / np.sum(y_train)
random_state = 0
classifiers = []
classifiers.append(LogisticRegression(random_state=random_state))
classifiers.append(SVC(random_state=random_state))
classifiers.append(KNeighborsClassifier(n_neighbors=3))
classifiers.append(GaussianNB())
classifiers.append(Perceptron(random_state=random_state))
classifiers.append(LinearSVC(random_state=random_state))
classifiers.append(SGDClassifier(random_state=random_state))
classifiers.append(DecisionTreeClassifier(random_state=random_state))
classifiers.append(RandomForestClassifier(n_estimators=100, random_state=random_state))
classifiers.append(AdaBoostClassifier(DecisionTreeClassifier(random_state=random_state), random_state=random_state, learning_rate=0.1))
classifiers.append(GradientBoostingClassifier(random_state=random_state))
classifiers.append(ExtraTreesClassifier(random_state=random_state))

cv_results = []
for classifier in classifiers :
  cv_results.append(cross_val_score(classifier, X_train, y=y_train, scoring='accuracy', cv=kfold, n_jobs=4))

cv_means = []
cv_std = []
for cv_result in cv_results:
  cv_means.append(cv_result.mean())
  cv_std.append(cv_result.std())

cv_res = pd.DataFrame({'CrossVal Means': cv_means, 'CrossVal Errors': cv_std,
                       'Algorithm': ['LogisticRegression', 'SVC', 'KNeighborsClassifier',
                                     'GaussianNB', 'Perceptron', 'LinearSVC', 'SGDClassifier',
                                     'DecisionTreeClassifier', 'RandomForestClassifier',
                                     'AdaBoostClassifier', 'GradientBoostingClassifier',
                                     'ExtraTreesClassifier']})

cv_res.sort_values(by=['CrossVal Means'], ascending=False, inplace=True)

cv_res
CrossVal MeansCrossVal ErrorsAlgorithm
00.9053570.105357LogisticRegression
30.8964290.094761GaussianNB
10.8928570.100699SVC
40.8910710.101157Perceptron
60.8910710.101157SGDClassifier
110.8785710.099360ExtraTreesClassifier
80.8660710.120387RandomForestClassifier
20.8446430.092461KNeighborsClassifier
50.8250000.107381LinearSVC
100.8017860.112783GradientBoostingClassifier
70.7464290.145248DecisionTreeClassifier
90.7089290.121389AdaBoostClassifier

回帰系のアルゴリズムでスコアが良い傾向があるように見える。

g = sns.barplot('CrossVal Means', 'Algorithm', data=cv_res, palette='Set3', orient='h', **{'xerr': cv_std})
g.set_xlabel('Mean Accuracy')
g = g.set_title('Cross validation scores')

2.パラメーターの最適化

続いて、いくつか選んだアルゴリズムのパラメーターを変更して、スコアの変化を見てみる。

### META MODELING WITH LR, RF and SGD

# Logistic Regression
LR = LogisticRegression(random_state=0)

## Search grid for optimal parameters
lr_param_grid = {'penalty': ['l1','l2']}

gsLR = GridSearchCV(LR, param_grid=lr_param_grid, cv=kfold, scoring='accuracy', n_jobs=4, verbose=1)

gsLR.fit(X_train, y_train)

LR_best = gsLR.best_estimator_

# Best score
gsLR.best_score_

Fitting 10 folds for each of 2 candidates, totalling 20 fits
0.9053571428571429

gsLR.best_params_

{‘penalty’: ‘l2’}

# Random Forest Classifier
RF = RandomForestClassifier(random_state=0)

## Search grid for optimal parameters
'''rf_param_grid = {'criterion': ['gini', 'entropy'],
                 'max_features': [1, 3, 10],
                 'min_samples_split': [2, 3, 10],
                 'min_samples_leaf': [1, 3, 10],
                 'n_estimators':[50, 100, 200]}'''
rf_param_grid = {'criterion': ['gini', 'entropy'],
                 'max_features': [40, 50, 60],
                 'min_samples_split': [2, 3, 10],
                 'min_samples_leaf': [1, 3, 10],
                 'n_estimators':[20, 30, 40]}

gsRF = GridSearchCV(RF, param_grid=rf_param_grid, cv=kfold, scoring='accuracy', n_jobs=4, verbose=1)

gsRF.fit(X_train, y_train)

RF_best = gsRF.best_estimator_

# Best score
gsRF.best_score_

Fitting 10 folds for each of 162 candidates, totalling 1620 fits
0.8946428571428573

gsRF.best_params_

{‘criterion’: ‘gini’,
‘max_features’: 50,
‘min_samples_leaf’: 3,
‘min_samples_split’: 2,
‘n_estimators’: 30}

# SGD Classifier
SGD = SGDClassifier(random_state=0)

## Search grid for optimal parameters
sgd_param_grid = {'alpha'  : [0.0001, 0.001, 0.01, 0.1],
                  'loss'   : ['log', 'modified_huber'],
                  'penalty': ['l2', 'l1', 'none']}

gsSGD = GridSearchCV(SGD, param_grid=sgd_param_grid, cv=kfold, scoring='accuracy', n_jobs=4, verbose=1)

gsSGD.fit(X_train, y_train)

SGD_best = gsSGD.best_estimator_

# Best score
gsSGD.best_score_

Fitting 10 folds for each of 24 candidates, totalling 240 fits
0.8660714285714285

gsSGD.best_params_

{‘alpha’: 0.0001, ‘loss’: ‘modified_huber’, ‘penalty’: ‘l1’}

3.学習曲線の確認

def plot_learning_curve(estimator, title, X, y, ylim=None, cv=None,
                        n_jobs=-1, train_sizes=np.linspace(.1, 1.0, 5)):
  '''Generate a simple plot of the test and training learning curve'''
  plt.figure()
  plt.title(title)
  if ylim is not None:
    plt.ylim(*ylim)
  plt.xlabel('Training examples')
  plt.ylabel('Score')
  train_sizes, train_scores, test_scores = learning_curve(
      estimator, X, y, cv=cv, n_jobs=n_jobs, train_sizes=train_sizes)
  train_scores_mean = np.mean(train_scores, axis=1)
  train_scores_std = np.std(train_scores, axis=1)
  test_scores_mean = np.mean(test_scores, axis=1)
  test_scores_std = np.std(test_scores, axis=1)
  plt.grid()

  plt.fill_between(train_sizes, train_scores_mean - train_scores_std,
                   train_scores_mean + train_scores_std, alpha=0.1,
                   color='r')
  plt.fill_between(train_sizes, test_scores_mean - test_scores_std,
                   test_scores_mean + test_scores_std, alpha=0.1,
                   color='g')
  plt.plot(train_sizes, train_scores_mean, 'o-', color='r',
           label='Training score')
  plt.plot(train_sizes, test_scores_mean, 'o-', color='g',
           label='Cross-validation score')

  plt.legend(loc='best')
  return plt

g = plot_learning_curve(gsLR.best_estimator_, 'LR mearning curves', X_train, y_train, cv=kfold)
g = plot_learning_curve(gsRF.best_estimator_, 'RF learning curves', X_train, y_train, cv=kfold)
g = plot_learning_curve(gsSGD.best_estimator_, 'SGD learning curves', X_train, y_train, cv=kfold)

Random Forestも良い気がしてきた。何とかトレーニングデータを増やしてもう少し確認したい。

最後に

全くトレーニングで使用していない未知のデータに対して、 Random Forest の予測モデルで判定してみたところ83%程の正解率だった。なろう小説のTop300はファンタジー系の小説が多いため、ファンタジー系の小説において、この正解率とみた方が良いかも知れない。ジャンルが違う小説の場合どのような結果になるか現時点では予測不能である。

Recommendation Systemを考える その2

Recommendation Systemを考えるの続き。

更新履歴:2021/03/19 データ処理に誤りがあったため訂正

初めに

小説をユーザーに推薦するかしないかで分けたいので、これは分類問題となる。分類問題についてはこちらで解説している。

分類するために、作者、関連作品やタグ付けも参照した方が精度が良くなるかもしれないが、今回はDoc2Vecのベクトル情報のみで行ってみる。Doc2Vec で作成した文書ベクトルを教師あり学習の説明変数として使用する。

文書ベクトルを教師あり学習の説明変数とした分類問題ではRandom Forestが有効という論文を見たので、まずは Random Forest を試してみる。また、今回使用するDoc2Vecの文書ベクトルは次元数が100ほどあるため、事前に次元圧縮を行うことも検討する。

最初は、このモデルを正しく評価するために、ユーザーの好みかの様なあいまいな指標で分類するのではなく、機械的に分類できる指標にする。

分類の実施

データセットの作成

以前作成した学習済みモデルを読み込み、データセットを作成する。

from gensim.models.doc2vec import Doc2Vec

MODEL_DATA_PATH = 'drive/My Drive/Colab Notebooks/syosetu/doc2vec_100.model'

m = Doc2Vec.load(MODEL_DATA_PATH)
vectors_list = [m.docvecs[n] for n in range(len(m.docvecs))]
import pandas as pd

MODEL_TITLES_CSV_PATH = 'drive/My Drive/Colab Notebooks/syosetu/novel_titles.csv'

df = pd.read_csv(MODEL_TITLES_CSV_PATH, header=None, names=['url', 'title', 'tag'])
df = df.drop(columns=['url'])
df['vectors'] = vectors_list

df.head(2)
titletagvectors
0転生したらスライムだった件R15 残酷な描写あり 異世界転生 スライム チート[5.356541, 1.455931, 1.0426528, 1.1359314, 3.9…
1無職転生 - 異世界行ったら本気だす –R15 残酷な描写あり 異世界転生[0.010144993, -12.32704, 2.8596005, 6.5284176,…

タグを見て機械的に分類したいため、どんなタグが存在するのか最初に確認する。

import collections
import itertools

tag = collections.Counter(list(itertools.chain.from_iterable(df['tag'].str.split().tolist())))
sorted(tag.items(), key=lambda pair: pair[1], reverse=True)[:20]
[('R15', 233),
 ('残酷な描写あり', 215),
 ('異世界転生', 98),
 ('異世界', 87),
 ('異世界転移', 86),
 ('チート', 84),
 ('ファンタジー', 80),
 ('魔法', 73),
 ('男主人公', 65),
 ('冒険', 63),
 ('主人公最強', 59),
 ('書籍化', 53),
 ('ハーレム', 48),
 ('成り上がり', 43),
 ('ほのぼの', 33),
 ('ハッピーエンド', 32),
 ('女主人公', 31),
 ('恋愛', 30),
 ('オリジナル戦記', 30),
 ('転生', 28)]

2極化しやすそうな、男主人公、女主人公の小説を抽出することにする。

df_m = df[df['tag'].str.contains('男主人公')]
df_m['female'] = 0
df_f = df[df['tag'].str.contains('女主人公|女性視点|悪役令嬢|後宮|少女マンガ')]
df_f['female'] = 1

上記の抽出方法の場合、女主人公と分類した小説の中に男主人公の小説が混じっている可能性がある。

df_f[df_f['tag'].str.contains('男主人公')]
titletagvectors
47乙女ゲー世界はモブに厳しい世界ですR15 残酷な描写あり 異世界転生 乙女ゲーム 悪役令嬢 冒険 人工知能 男主人公 学園 …[6.775977, 4.3698134, -1.9109731, 9.731645, -0…
74【Web版】町人Aは悪役令嬢をどうしても救いたい【本編完結済み】R15 残酷な描写あり 異世界転生 乙女ゲーム 悪役令嬢 オリジナル戦記 男主人公 西洋 …[2.9941628, 1.892081, 1.9338807, 4.0745897, 6….

上記は男主人公の小説と思われるため、女主人公の小説の一覧から除外する。

df_f = df_f.drop([47, 74])
df = pd.concat([df_m, df_f])
df['female'].value_counts()

0 67
1 51
Name: female, dtype: int64

男主人公の小説の方が数が多いため、女主人公の小説の数と合わせる。

#男主人公の一覧
df_0 = df[df['female'] == 0]
#女主人公の一覧
df_1 = df[df['female'] == 1]

df_0 = df_0.sample(n=len(df_1), random_state=0)

df = pd.concat([df_0, df_1])
機械学習の実施

データセットの作成が出来たので、 Random Forest を用いて、分類を実施してみる。

X = df['vectors']
y = df['female']

X_array = np.array([np.array(v) for v in X])
y_array = np.array([i for i in y])
from sklearn.model_selection import train_test_split

X_train, X_test, y_train, y_test = train_test_split(X_array, y_array, random_state=0)
from sklearn.ensemble import RandomForestClassifier

random_forest = RandomForestClassifier(random_state=0)
random_forest.fit(X_train, y_train)

random_forest.score(X_test, y_test)

0.8076923076923077

random_forest.predict(X_test)

array([0, 1, 0, 1, 1, 1, 0, 0, 1, 1, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0,
0, 1, 0, 0])

試しに、ロジスティック回帰でも実施してみる。

from sklearn.linear_model import LogisticRegression

logreg = LogisticRegression()
logreg.fit(X_train, y_train)

logreg.score(X_test, y_test)

0.9230769230769231

logreg.predict(X_test)

array([0, 1, 0, 1, 1, 1, 0, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0])

ロジスティック回帰の方が良いスコアになった。しかし、過学習を起こしている可能性もある。

まとめ

Random Forest が一番良い結果になると予想していたが、違った。
データセットは大体問題なさそうなので、この問題を解くにはどの分類器が適しているのか、また最適なパラメーターを探す作業は次回行いたいと思う。

複数の機械学習アルゴリズムを比較するへ続く。

分類問題を機械学習で解く

今回の目的

理論とか細かいことは後回し、取り敢えず実行してみて、機械学習がどのようなものか感じをつかむ。

分類問題とは

データを複数のクラスに分類すること。

今回は、機械学習の手法の一つである教師あり学習を使って分類する。教師あり学習とは、予め用意された問題と正解の傾向を学習することで、未知の問題に対する正解を推測する手法をいう。

必要なもの

  • データセット
  • 分類器 (データを分類する機械学習モデル)
    • k-近傍法(k-NN)
    • 決定木
    • サポートベクターマシン(SVM)
    • ロジスティック回帰など

データの傾向を学習させる必要があるため、目的に合わせたデータセットを事前に用意する。分類器はライブラリとして既に実装されているものを利用する。

機械学習を試すとき、まずはデータを準備することが一つのハードルになるが、scikit-learnにはいくつかの標準的なデータセットが付属しているので、自分で用意しなくても試すことが出来る。

今回はsklearn.datasetsのload_iris(アヤメの花のデータセット)を使用する。がくや花びらの大きさとアヤメの種類がデータに含まれていて、がくや花びらの大きさからアヤメの種類を予測する。

Pythonで実装

from sklearn.datasets import load_iris

iris = load_iris()
# データの内容を確認する
iris
{'DESCR': '(データの説明は省略)',
 'data': array([[5.1, 3.5, 1.4, 0.2],
        [4.9, 3. , 1.4, 0.2],
        [4.7, 3.2, 1.3, 0.2],
        [4.6, 3.1, 1.5, 0.2],
        [5. , 3.6, 1.4, 0.2],
        (中略)
        [5.9, 3. , 5.1, 1.8]]),
 'feature_names': ['sepal length (cm)', # がくの長さ
  'sepal width (cm)',                   # がくの幅
  'petal length (cm)',                  # 花びらの長さ
  'petal width (cm)'],                  # 花びらの幅
 'filename': '/usr/local/lib/python3.6/dist-packages/sklearn/datasets/data/iris.csv',
 # アヤメの種類 0: 'setosa', 1: 'versicolor', 2: 'virginica'
 'target': array([0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
        0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
        0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
        1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
        1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2,
        2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2,
        2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2]),
 'target_names': array(['setosa', 'versicolor', 'virginica'], dtype='<U10')} 

この後のデータの解析を行いやすくするため、pandasのDataFrameに変換する。

import pandas as pd

# DataFrameに変換する
df = pd.DataFrame(iris.data, columns=iris.feature_names)
df['target'] = iris.target_names[iris.target]

# 先頭5行を表示する
df.head()
sepal length (cm)sepal width (cm)petal length (cm)petal width (cm)target
05.13.51.40.2setosa
14.93.01.40.2setosa
24.73.21.30.2setosa
34.63.11.50.2setosa
45.03.61.40.2setosa

ざっとデータの内容を確認する。欠損値もないしそのまま使えるように既に整えられている。

df.info()

RangeIndex: 150 entries, 0 to 149
Data columns (total 5 columns):
 #   Column             Non-Null Count  Dtype  
---  ------             --------------  -----  
 0   sepal length (cm)  150 non-null    float64
 1   sepal width (cm)   150 non-null    float64
 2   petal length (cm)  150 non-null    float64
 3   petal width (cm)   150 non-null    float64
 4   target             150 non-null    object 
dtypes: float64(4), object(1)
memory usage: 6.0+ KB
df.describe()
sepal length (cm)sepal width (cm)petal length (cm)petal width (cm)
count150150150150
mean5.8433333.0573333.7581.199333
std0.8280660.4358661.7652980.762238
min4.3210.1
25%5.12.81.60.3
50%5.834.351.3
75%6.43.35.11.8
max7.94.46.92.5
df['target'].value_counts()

virginica     50
setosa        50
versicolor    50
Name: target, dtype: int64

次に説明変数と目的変数に分ける。
説明変数とは目的変数を説明する変数のこと。これをもとに予測する。
目的変数とは予測したい変数のこと。

# 説明変数
X = df.drop('target', axis=1)
# 目的変数
y = df['target']

さらに、トレーニング用データセットとテスト用データセットに分ける。未知の値について予測する性能をテストするため、テスト用のデータはトレーニングで使用してはいけない。

from sklearn.model_selection import train_test_split

X_train, X_test, y_train, y_test = train_test_split(X, y, random_state=0)

分類器にはサポートベクターマシンを使用する。scikit-learnサポートベクターマシンを実装したライブラリがあるので、それをそのまま使用する。

from sklearn.svm import SVC

svc = SVC()
# 問題と正解の傾向を学習させ、学習済みモデルを作成する
svc.fit(X_train, y_train)

学習済みモデルが作成出来たら、テストデータを使ってアヤメの種類を予測してみる。

svc.predict(X_test)

array(['virginica', 'versicolor', 'setosa', 'virginica', 'setosa',
       'virginica', 'setosa', 'versicolor', 'versicolor', 'versicolor',
       'virginica', 'versicolor', 'versicolor', 'versicolor',
       'versicolor', 'setosa', 'versicolor', 'versicolor', 'setosa',
       'setosa', 'virginica', 'versicolor', 'setosa', 'setosa',
       'virginica', 'setosa', 'setosa', 'versicolor', 'versicolor',
       'setosa', 'virginica', 'versicolor', 'setosa', 'virginica',
       'virginica', 'versicolor', 'setosa', 'virginica'], dtype=object)

どのくらい正解しているのかは下記で確認出来る。

svc.score(X_test, y_test)

0.9736842105263158

97%正しいアヤメの種類を予測出来ている。

まとめ

上記はかなり単純な例です。このように高い率で予測出来ているのは、使用しやすく既にデータが整備されていたためです。本来データには欠損値であったり、ノイズであったり、そもそも予測するための説明変数が不足していたりしていますので、まず学習で使用するデータの作成に時間がかかります。
また、分類器も今回はサポートベクターマシンを使用しましたが、最適な分類器ではないかもしれません。どの分類器が最適か探すこともあるでしょうし、そのパラメーターのチューニングも必要になるかもしれません。
これらを詰めていき正解率を上げていく作業はなかなか楽しいです。興味があればさらに詳しく調べてみてください。

Recommendation Systemを考える

以下の機械学習の勉強の過程で小説の特徴を抽出することは出来た。ここからユーザーにおすすめの小説を推薦するにはどのような仕組みが必要なのか考える。

小説を読もうの累計ランキングをDoc2Vecで解析する その5

Recommendation System(推薦システム)とは

過去の行動をもとに、あるアイテムを好む可能性を予測するシステム。
例えば、Netflixでは、映画を見たり、投票したりするような過去の行動に応じて、新しい映画を提案している。
新しいアイテムを推奨するという点が重要となる。

・ユーザーベースの推薦システム

自分の経験と他の人の経験を組み合わせて推薦する。ユーザ間の類似度を計算し、類似度の高いユーザーが好む作品の中でまだ見ていない作品を推薦する。

このシステムを実現するには複数のユーザー情報が必要になるため、今回は不採用。

・アイテムベースの推薦システム

類似のユーザを見つけるのではなく、映画や物などのアイテムを比較する。アイテム間の類似度を計算する。

・どのような推薦システムを作成するか

ユーザーが入力した小説に似ている小説を推薦するシステムは以前作成したDoc2Vecのモデルを使用すれば実現可能である。しかし、このシステムではユーザーが毎回小説を入力しなければいけない点が煩わしい。

例えば、最近投稿された小説の中から自分の好みに合う小説があったら、自動的に通知が来るようなシステムであると良い。

このシステムは以前作成したモデルのみでは実現不可能であり、さらに自分の好みを学習させる必要がある。これまでと違い教師あり学習になるので、どのような手法があるか調べるところから始めることにする。

Recommendation Systemを考える その2へ続く。