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.まとめ

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

mecab設定の覚え書き

新環境を構築するとき忘れがちなので、覚え書きとして残しておく。

1.インストール

sudo apt update
sudo apt upgrade
sudo apt install mecab libmecab-dev mecab-ipadic-utf8 git make
curl xz-utils file
sudo apt install python3-pip
pip3 install mecab-python3 unidic-lite neologdn
git clone --depth 1 https://github.com/neologd/mecab-ipadic-neologd.git
echo yes | mecab-ipadic-neologd/bin/install-mecab-ipadic-neologd -n -a

Usage:
    $ mecab -d /usr/lib/x86_64-linux-gnu/mecab/dic/mecab-ipadic-neologd ...

2.使用方法

import re
import neologdn
import subprocess
import MeCab

CONTENT_WORD_POS = ('名詞', '動詞', '形容詞', '形容動詞', '副詞')
STOP_WORD_POS = ('代名詞', '助動詞', '非自立', '数', '人名')

# 正規化
def normalize(text):
  return neologdn.normalize(re.sub(r'[0-9]+', '0', text).lower())

# 形態素解析
def analysis(text):
  cmd = 'echo `mecab-config --dicdir`"/mecab-ipadic-neologd"'
  path = (subprocess.Popen(cmd, stdout=subprocess.PIPE, shell=True).communicate()[0]).decode('utf-8')
  tagger = MeCab.Tagger('-d {0}'.format(path))
  tagger.parse('')
  node = tagger.parseToNode(normalize(text))
  word = ''
  pre_features = []

  while node:
    features = node.feature.split(',')[:6]
    # 名詞、動詞、形容詞、形容動詞、副詞であるか判定
    valid = False
    for pos in CONTENT_WORD_POS:
      valid = pos in features or valid
    # 以下に該当する場合は除外(ストップワード)
    for pos in STOP_WORD_POS:
      valid = (not pos in features) and valid
    if valid:
      if ('名詞接続' in pre_features and '名詞' in features) or ('接尾' in features):
        word += '{0}'.format(node.surface)
      else:
        word += ' {0}'.format(node.surface)
      #print('{0} {1}'.format(node.surface, features))
    pre_features = features
    node = node.next
  return word[1:]

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

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

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

Windows Subsystem for Linux 2でCUDAを使えるようにする

1.cuda toolkitをインストール

sudo apt-key adv --fetch-keys 
http://developer.download.nvidia.com/compute/cuda/repos/ubuntu2004/x86_64/3bf863cc.pub
cat <<EOF | sudo tee /etc/apt/sources.list.d/cuda.list
deb https://developer.download.nvidia.com/compute/cuda/repos/ubuntu2004/x86_64 /
EOF
sudo apt update
sudo apt install cuda-toolkit-11-5

2.libcudnn8、libncclをインストール(任意)

sudo apt-key adv --fetch-keys https://developer.download.nvidia.com/compute/machine-learning/repos/ubuntu2004/x86_64/7fa2af80.pub
cat <<EOF | sudo tee /etc/apt/sources.list.d/cuDNN.list
deb https://developer.download.nvidia.com/compute/machine-learning/repos/ubuntu2004/x86_64 /
EOF
sudo apt update
sudo apt install libcudnn8 libnccl-dev

3.動作確認

Tensorflowをインストールする。

pip install --upgrade tensorflow

以下を実行する。

from tensorflow.python.client import device_lib

device_lib.list_local_devices()

実行結果の一部に下記が表示されていれば正常に導入できている。

 name: "/device:GPU:0"
 device_type: "GPU"

Windows上にPythonの開発環境を構築する

前提

Windows Subsystem for Linuxを設定し、Ubuntuのインストールが済んでいること。

1. VSCodeをインストール

Windows版をダウンロードし、インストールする。
Windows Subsystem for Linuxがインストール済みであれば、初回起動時に下記が表示されるため、インストールする。

一度、VSCodeを終了し、Ubuntuコンソール上で下記を実行する。

code .

VSCodeが起動した後、下記の「Python」をクリックして、インストールする。

Python 拡張機能のインストールが完了すると、さらに下記が表示されるので、インストールする。

2. 動作確認

VSCode上で下記の通り記入したhello.pyファイルを作成する。

msg = 'Hello World'
print(msg)

Run Python File in Terminalをクリックし、実行結果が表示されれば動作確認完了。

xxx@xxx:~/test$ /usr/bin/python3 /home/xxx/test/hello.py
Hello World

Windows Subsystem for Linux 2でPythonを動かす

1. Windows Subsystem for Linuxをインストール

Windows Power Shellを管理者権限で開き、下記を実行する。

Enable-WindowsOptionalFeature -Online -FeatureName Microsoft-Windows-Subsystem-Linux

WSL 2を使用するため、仮想マシンプラットフォームを有効化する。

Enable-WindowsOptionalFeature -Online -FeatureName VirtualMachinePlatform

x64 マシン用 WSL2 Linux カーネル更新プログラム パッケージをインストールする。

デフォルトバージョンを2とする。

wsl --set-default-version 2

2. Ubuntu Linuxをインストール

Microsoft StoreからUbuntuをインストールする。

3. WSLのバージョン確認

> wsl -l -v
  NAME      STATE           VERSION
* Ubuntu    Running         2

4. Pythonのバージョン確認

Ubuntuをインストールした場合、Python3が既にインストールされている。

$ python3 --version
Python 3.8.5

Kubernetes環境構築

Raspberry Pi 2上に構築しようと思っていたのだが、メモリが足りなくてまともに使用するのは無理そうなため、取り合えず仮想環境上に構築したメモ。

1. スワップ無効化

無効化しなくても動くらしいが、パフォーマンスを良くするのために、無効化が推奨らしい。

2. dockerインストールに必要なソフトウェアをインストール
sudo apt install apt-transport-https ca-certificates curl software-properties-common
sudo apt update
sudo apt upgrade
3. dockerインストール
curl -fsSL https://download.docker.com/linux/ubuntu/gpg | sudo apt-key add -
sudo add-apt-repository "deb [arch=amd64] https://download.docker.com/linux/ubuntu focal stable"
sudo apt update
apt-cache policy docker-ce
#Kubernetesの動作保証はv19までであるが、今回は最新バージョンで試した
sudo apt install docker-ce
sudo systemctl status docker
#dockerの操作をsudo無しで出来るようにするため、ログインユーザーをdockerグループに追加
sudo usermod -aG docker ユーザーID
4. docker設定
cat <<EOF | sudo tee /etc/docker/daemon.json
{
  "exec-opts": ["native.cgroupdriver=systemd"],
  "log-driver": "json-file",
  "log-opts": {
    "max-size": "100m"
  },
  "storage-driver": "overlay2"
}
EOF

sudo mkdir -p /etc/systemd/system/docker.service.d
sudo systemctl daemon-reload
sudo systemctl restart docker
#ブリッジ接続を有効化
sudo sysctl net.bridge.bridge-nf-call-iptables=1

#カーネルオプションに下記を設定し再起動
sudo vi /etc/default/grub
GRUB_CMDLINE_LINUX="cgroup_enable=memory swapaccount=1"
sudo update-grub
sudo shutdown -r now
5. kubernetesインストール
curl -s https://packages.cloud.google.com/apt/doc/apt-key.gpg | sudo apt-key add -
cat <<EOF | sudo tee /etc/apt/sources.list.d/kubernetes.list
deb https://apt.kubernetes.io/ kubernetes-xenial main
EOF

sudo apt update
sudo apt install kubelet kubeadm kubectl
6. kubernetes設定(マスター)
sudo kubeadm init --apiserver-advertise-address=192.168.xx.xx
mkdir -p $HOME/.kube
sudo cp -i /etc/kubernetes/admin.conf $HOME/.kube/config
sudo chown $(id -u):$(id -g) $HOME/.kube/config
#Weave Netを設定
kubectl apply -f "https://cloud.weave.works/k8s/net?k8s-version=$(kubectl version | base64 | tr -d '\n')"
7. kubernetes設定(マスター以外)

dockerのインストール、設定共にマスターと同様に実施する。5. kubernetesのインストールまで同様に実施する。

sudo kubeadm join 192.168.xx.xx:6443 --token 「initしたときに表示されたトークン」 --discovery-token-ca-cert-hash 「initしたときに表示されたハッシュコード」
8. 確認
#マスターでノードが追加されたか確認
kubectl get nodes
#ラベルを付ける
kubectl label node 「ノード名」 node-role.kubernetes.io/worker=

Raspberry Pi 2でクラスタ構築 その4(Hadoop編)

前回、HDFS高可用性(HA)機能を設定した。今回はResourceManagerをHA化する。

準備

SDカードの寿命を縮めるため、あまり推奨は出来ないが、本格的にメモリ不足のため、念のためスワップ領域を作成しておく。

sudo mkdir /var/swap
sudo dd if=/dev/zero of=/var/swap/swap0 bs=1M count=2048
sudo chmod 600 /var/swap/swap0
sudo mkswap /var/swap/swap0
sudo swapon /var/swap/swap0
#再起動後も自動的にスワップ領域が適用されるようにする
sudo vi /etc/fstab
#下記の行を追加する
/var/swap/swap0 none swap defaults 0 0

2021/02/25 追記:
SDカードへ書き込みを少なくするため、スワップの頻度が少なくなるように調整する。

#スワップの閾値を確認
cat /proc/sys/vm/swappiness
> 60

sudo vi /etc/sysctl.conf
#下記を追記する(物理メモリが1%以下になったらスワップを開始)
vm.swappiness = 1

#設定を反映
sudo sysctl -p
cat /proc/sys/vm/swappiness
> 1

サーバー構成を検討

NameNodeは奇数で構成する必要があったが、ResourceManagerはそのような制約はないため、下記の2台構成とする。

master1・・・Zookeeper,NameNode(Standby), ResourceManager(Standby)
master2・・・Zookeeper,NameNode(Standby), ResourceManager(Active)
master3・・・Zookeeper,NameNode(Active)
slave1・・・DataNode, NodeManager
slave2・・・DataNode, NodeManager
slave3・・・DataNode, NodeManager
slave4・・・DataNode, NodeManager

ResourceManager高可用性(HA)機能の設定

master1~2のetc/hadoop/yarn-site.xmlを編集する。(メモリ関連の記述は省略)

   <property>
    <name>yarn.resourcemanager.ha.enabled</name>
    <value>true</value>
  </property>
  <property>
    <name>yarn.resourcemanager.cluster-id</name>
    <value>rpcluster_r</value>
  </property>
  <property>
    <name>yarn.resourcemanager.ha.rm-ids</name>
    <value>rm1,rm2</value>
  </property>
  <property>
    <name>yarn.resourcemanager.hostname.rm1</name>
    <value>master1</value>
  </property>
  <property>
    <name>yarn.resourcemanager.hostname.rm2</name>
    <value>master2</value>
  </property>
  <property>
    <name>yarn.resourcemanager.webapp.address.rm1</name>
    <value>master1:8088</value>
  </property>
  <property>
    <name>yarn.resourcemanager.webapp.address.rm2</name>
    <value>master2:8088</value>
  </property>
  <property>
    <name>yarn.resourcemanager.scheduler.class</name>
    <value>org.apache.hadoop.yarn.server.resourcemanager.scheduler.fair.FairScheduler</value>
  </property>
  <property>
    <name>yarn.resourcemanager.recovery.enabled</name>
    <value>true</value>
  </property>
  <property>
    <name>yarn.resourcemanager.store.class</name>
    <value>org.apache.hadoop.yarn.server.resourcemanager.recovery.ZKRMStateStore</value>
  </property>
  <property>
    <name>hadoop.zk.address</name>
    <value>master1:2181,master2:2181,master3:2181</value>
  </property>

slave1~4のetc/hadoop/yarn-site.xmlを編集する。(メモリ関連の記述は省略)

   <property>
    <name>yarn.resourcemanager.ha.enabled</name>
    <value>true</value>
  </property>
  <property>
    <name>yarn.resourcemanager.cluster-id</name>
    <value>rpcluster_r</value>
  </property>
  <property>
    <name>yarn.resourcemanager.ha.rm-ids</name>
    <value>rm1,rm2</value>
  </property>
  <property>
    <name>yarn.resourcemanager.hostname.rm1</name>
    <value>master1</value>
  </property>
  <property>
    <name>yarn.resourcemanager.hostname.rm2</name>
    <value>master2</value>
  </property>
  <property>
    <name>yarn.resourcemanager.webapp.address.rm1</name>
    <value>master1:8088</value>
  </property>
  <property>
    <name>yarn.resourcemanager.webapp.address.rm2</name>
    <value>master2:8088</value>
  </property>
  <property>
    <name>yarn.resourcemanager.scheduler.class</name>
    <value>org.apache.hadoop.yarn.server.resourcemanager.scheduler.fair.FairScheduler</value>
  </property>
  <property>
    <name>yarn.resourcemanager.recovery.enabled</name>
    <value>true</value>
  </property>
  <property>
    <name>yarn.resourcemanager.store.class</name>
    <value>org.apache.hadoop.yarn.server.resourcemanager.recovery.ZKRMStateStore</value>
  </property>
  <property>
    <name>hadoop.zk.address</name>
    <value>master1:2181,master2:2181,master3:2181</value>
  </property>
  <property>
    <name>yarn.nodemanager.aux-services</name>
    <value>mapreduce_shuffle</value>
  </property>
  <property>
    <name>yarn.nodemanager.env-whitelist</name>
    <value>JAVA_HOME,HADOOP_COMMON_HOME,HADOOP_HDFS_HOME,HADOOP_CONF_DIR,CLASSPATH_PREPEND_DISTCACHE,HADOOP_YARN_HOME,HADOOP_MAPRED_HOME</value>
  </property>

ResourceManagerの起動

master1で下記コマンドを実行し、ResourceManagerを起動する。

$HADOOP_HOME/sbin/start-yarn.sh

起動が確認できたので、実際に処理を実行してみる。

$HADOOP_HOME/bin/hadoop jar /home/xxx/hadoop-latest/share/hadoop/mapreduce/hadoop-mapreduce-examples-3.2.2.jar pi 10 10000

最終的に下記の結果が表示された。スワップを設定したからか今回は前回に比べて50秒ほど早く処理が終わった。

Job Finished in 256.114 seconds
Estimated value of Pi is 3.14120000000000000000