Kaggleのタイタニック問題で、スコア80.143までの記録(ランダムフォレスト) | そう備忘録

Kaggleのタイタニック問題で、スコア80.143までの記録(ランダムフォレスト)

kaggleのタイタニック問題

Kaggle の入門コンペティションのタイタニック問題に挑戦した時の記事。

前回の記事では Neural Network で生存者予測を行ったが今回はRandomForest(ランダムフォレスト)で予測を行ってみる。

そして Neural Network とランダムフォレストでのそれぞれの生存確率の平均(アンサンブル学習)から新たな提出用データを作成して、それぞれのモデルの正解率と比較を行った。

興味深いことに個々の予測が 0.80 を下回っていても平均から予測データを作成すると 0.80 を超えた。

アンサンブル予測

ランダムフォレスト

今回の記事はイモリの表紙の書籍「scikit-learn、Keras、TensorFlowによる実践機械学習 第2版」の6章(決定木)、7章(アンサンブル学習とランダムフォレスト)を参考にさせて貰った。

Deep Neural Network の時も参考にさせてもらったがランダムフォレストも非常に参考になった。

ちなみにアンサンブル学習については上記の書籍で以下の記述がある。

「数千、数万人に複雑な問題の答えを尋ねて答えを集計すると一人の専門家の答えより良いことがある、これを集合知(wisdom of crowd)という」

複数の学習器での集計値が必ずしも高い正解率になるとは限らないが今回はわずかながらだが上昇した。

データ分析

事前のデータ分析に関してはNeural Network で生存率を予測した以前の記事を参照して欲しい。

ソースコード

rf_data.py

ランダムフォレスト用のデータ作成のモジュール。

複数箇所で使用するので独自モジュールにしている。

# -*- coding: utf-8 -*-
"""
Created on Mon Nov 22 08:39:54 2021

・RandomForest用のデータ作成

@author: Souichirou Kikuchi
"""

def rf_data(drive, r_state):
    from sklearn.preprocessing import LabelEncoder # 文字列を数値に置き換える
    from sklearn.model_selection import train_test_split # データを分割
    import re # 正規表現
    import pandas as pd
    def ticket_alpha(df): # Ticketアルファベット
        s = df['Ticket'] 
        if s.isnumeric(): # 数字
            result = 'ZZZ' # 数字のみのチケット
        else:
            rtn = re.findall(r'\w+', s) # アルファベット、アンダーバー、数字(複数の時は配列)
            rslt = ''
            if len(rtn) > 1:
                for i in range(len(rtn)-1): # 最後の配列は数字なので入れない
                    rslt += rtn[i]
            else:
                rslt = rtn[0] # 英字だけのTicket
            result = rslt
        return result
    
    def ticket_num(df): # Ticket数字
        s = df['Ticket'] 
        if s.isnumeric(): # 数字
            result = float(s)
        else:
            rtn = re.findall(r'\d+', s) # 数字部分を取り出し(複数の時は配列)
            if len(rtn) > 0:
                result = float(rtn[len(rtn)-1]) # 複数取り出した時は最後を使う
            else:
                result = float(999999) # 英字のみのTicketは999999をセット
        return result
    
    train_csv = pd.read_csv(drive + '/csv/train.csv', index_col=False) # CSVの読み込み
    test_csv = pd.read_csv(drive + '/csv/test.csv', index_col=False) # CSVの読み込み
    test_id = test_csv['PassengerId'] # 提出用にテストのPassengerIdを取り出しておく
    tr_ts = pd.concat([train_csv, test_csv], ignore_index=True) # 結合する

    # Age、FareがNANのデータを男女、Pclass別の平均で穴埋め
    sx_list = ['male', 'female'] # Sex
    pc_list = [1, 2, 3] # Pclass
    for sx in sx_list:
        for pl in pc_list:
            tr_ts.loc[(tr_ts['Sex'].values == sx) & (tr_ts['Pclass'].values == pl) & (tr_ts['Age'].isnull()), 'Age']   = tr_ts.query('Sex == "' + sx + '" & Pclass == ' + str(pl))['Age'].mean()
            tr_ts.loc[(tr_ts['Sex'].values == sx) & (tr_ts['Pclass'].values == pl) & (tr_ts['Fare'].isnull()), 'Fare']   = tr_ts.query('Sex == "' + sx + '" & Pclass == ' + str(pl))['Fare'].mean()

    # Embarked(乗船場所)は最頻値で置き換え
    tr_ts.fillna({'Embarked': tr_ts['Embarked'].mode()[0]}, inplace=True)

    # Ticketのアルファベット部・整形
    tr_ts['Ticket_alpha'] = tr_ts.apply(ticket_alpha, axis=1)

    # Ticketの数字部・整形
    tr_ts['Ticket_num'] = tr_ts.apply(ticket_num, axis=1)

    # Cabin整形 先頭の1文字
    tr_ts.fillna({'Cabin': 'Z'}, inplace=True) # NanはZ埋め
    tr_ts['Cabin_alpha'] = tr_ts['Cabin'].str[:1]

    # Nameから敬称(Mr.等を取り出す)
    tr_ts['Name_ttl'] = tr_ts['Name'].str.extract(r'(\S+)\.', expand=False) # ピリオドで終わる文字列を取り出す

    # Nameから姓を取り出す
    tr_ts['Family_name'] = tr_ts['Name'].str.extract(r'(\S+)\,', expand=False) # カンマの前の文字列を取り出す

    # 性別、乗船場所、チケットアルファベット、キャビン(最初の1文字)、名前の敬称、姓を数値化
    for col in ['Sex', 'Embarked', 'Ticket_alpha', 'Cabin_alpha', 'Name_ttl', 'Family_name']:
        le = LabelEncoder()
        le.fit(tr_ts[col])
        tr_ts[col] = le.transform(tr_ts[col])

    # Ticket再整形(アルファベット部と数字部分の結合)
    tr_ts['Ticket_ttl'] = (tr_ts['Ticket_alpha'] * 10**6) + tr_ts['Ticket_num']

    # 姓とFare(料金)を結合・家族を特定する
    tr_ts['Family_fare'] = tr_ts['Family_name'] * 10**3 + tr_ts['Fare']

    # Fare(料金)をSibSp+Parch + 1で割る
    tr_ts['Fare'] = tr_ts['Fare'] / (tr_ts['SibSp'] + tr_ts['Parch'] + 1)

    # SibSp+ParchでFamily_num(家族数)項目を作る
    tr_ts['Family_num'] = tr_ts['SibSp'] + tr_ts['Parch']

    # One Hot Encordeing ランダムフォレストで追加した
    for col in ['Pclass', 'Embarked', 'Cabin_alpha']:
        tr_ts = pd.get_dummies(tr_ts, columns=[col])

    # その他使用しないカラムを削除する
    drop_list = ['Name', 'Ticket', 'Ticket_alpha', 'Ticket_num', 'Cabin', 'SibSp', 'Parch', 'Family_name']
    for dl in drop_list:
        tr_ts.drop(columns=[dl], inplace=True)
    
    # trainとtestの分割
    train = tr_ts[:891].copy() 
    test = tr_ts[891:].copy()

    survived = train.Survived.values # 生存者カラムを取り出す(正解データ)

    # SurvivedとPassengerIdを削除する
    drop_list = ['Survived', 'PassengerId']
    for dl in drop_list:
        train.drop(columns=[dl], inplace=True)
        test.drop(columns=[dl], inplace=True)

    # trainを学習と検証用に分割
    if r_state == True:
        X_train, X_vali, y_train, y_vali = train_test_split(train,
                                                        survived, # 生存者データ
                                                        train_size = 0.8, # trainibg 
                                                        test_size = 0.2, # Validation
                                                        random_state = 42, # 0:shuffleを固定
                                                        shuffle = True) # shuffle=True(取り出す前にシャッフルする)
    else: # random_state未指定
        X_train, X_vali, y_train, y_vali = train_test_split(train,
                                                        survived, # 生存者データ
                                                        train_size = 0.8, # trainibg 
                                                        test_size = 0.2, # Validation
                                                        shuffle = True) # shuffle=True(取り出す前にシャッフルする)

    return X_train, X_vali, y_train, y_vali, test, test_id
rf_data 関数

以下の項目を特徴量抽出に使用することにした。

  • Age(年齢)
  • Fare(旅客運賃)
  • Pclass(チケットクラス)
  • Embarked(乗船場所)
  • Ticket(チケット)
  • Cabin(キャビン番号)
  • Name(名前)
  • SibSp(兄弟、配偶者の数)
  • Parch(親、子供の数)
Age、Fareの欠損値を埋める

Age(年齢)、Fare(旅客運賃)の欠損値を埋める。

Deep Neural Network の時は Age を推測するネットワークを構築して予測値を埋めたが今回(ランダムフォレスト)では Sex(性別)、Pclass(チケットクラス)別の平均値で欠損値を埋めた。

    # Age、FareがNANのデータを男女、Pclass別の平均で穴埋め
    sx_list = ['male', 'female'] # Sex
    pc_list = [1, 2, 3] # Pclass
    for sx in sx_list:
        for pl in pc_list:
            tr_ts.loc[(tr_ts['Sex'].values == sx) & (tr_ts['Pclass'].values == pl) & (tr_ts['Age'].isnull()), 'Age']   = tr_ts.query('Sex == "' + sx + '" & Pclass == ' + str(pl))['Age'].mean()
            tr_ts.loc[(tr_ts['Sex'].values == sx) & (tr_ts['Pclass'].values == pl) & (tr_ts['Fare'].isnull()), 'Fare']   = tr_ts.query('Sex == "' + sx + '" & Pclass == ' + str(pl))['Fare'].mean()
その他の特徴量

データの加工方法は Neural Network の時とほぼ同じだ。

Embarked(乗船場所)、Ticket、Cabin、Nameの敬称、Nameの家族名(姓)、料金、家族数を使用している。

Embarked の欠損値は最頻値で埋めている。

Ticket は英字部分と数字に分けて集計、Cabin は最初の1文字目の英字を使用している。

Name欄からは敬称(Mr.やMiss)を抽出、Name欄から取り出した家族名(姓)と料金を組み合わせて Family_fare の項目を作っている。

それぞれの項目についての詳細は以前の記事を参照して欲しい。

尚、Pclass、Embarked、Cabin_alpha については One Hot Encordeing している。

また先程の書籍ではランダムフォレストではスケーリングは不要とあったので行っていない。

titanic_rft_optimize.py

学習する前に RandomSearchCV で最適なハイパーパラメータを検索する。

# -*- coding: utf-8 -*-
"""
Created on Tue Sep 28 14:59:31 2021

・Kagle タイタニック生存者予測(学習)
・ランダムフォレスト
・GridSearchCVで最適なハイパーパラメータを検索する

@author: Souichirou Kikuchi
"""

# GDRIVE = '/content/drive/MyDrive/M2B/Program/Kaggle/Titanic' # Google colab
GDRIVE = '.' # Local Python

import sys # 自作モジュールへのPathを追加する(colabで必要)
ROOT_PATH = GDRIVE
sys.path.append(ROOT_PATH)

from rf_data import rf_data # データ前処理
from sklearn.model_selection import GridSearchCV
from sklearn.ensemble import RandomForestClassifier

# main
X_train, X_vali, y_train, y_vali, test, test_id = rf_data(drive = GDRIVE, r_state = True) # データ加工(特徴量抽出)

# 学習に関するパラメータ設定
tuned_parameters = {
    'n_estimators' : [2, 5, 10, 15, 20, 30, 50, 75, 100, 200, 500, 1000],
    'max_depth' : [2, 3, 5, 10, 15, 20, 30],
    'min_samples_split' : [2, 3, 5, 10, 15, 20, 30]
}

model = GridSearchCV(estimator=RandomForestClassifier(criterion='gini', random_state=1, verbose=1), # GridSearch
                     param_grid=tuned_parameters, # Turning Parameter
                     cv=3)

model = model.fit(X_train, y_train)

print ('最適パラメータ:{}'.format(model.best_params_))
model = model.best_estimator_

# 検証用セットを用いて評価
print('score = ', model.score(X_vali, y_vali))

GDRIVE

このプログラムをGoogle Colabo で動作させる時の定数。

普段はノートパソコンの Spyder で試行錯誤していたが出先で Colabo で確認したい時もあったので、その際は GDRIVE のコメントアウトで動作環境を切り替えた。

n_estimators

木の数を指定するハイパーパラメータ。

最適値を検索している。

max_depth

深さの上限を指定するハイパーパラメータ。

最適値を検索している。

上限を制限しないと(デフォルトは無制限)過学習のリスクが増加する。

min_samples_split

ノードを分割するために必要なサンプル数の下限を指定するハイパーパラメータ。

こちらも最適値を検索している。

criterion

criterion(基準)は entropy(エントロピー)も指定が可能だが gini(不純度)を指定した。

先程の書籍には「ほとんど場合、どちらを使っても大差ない」「ジニ不純度の方が僅かに高速」「ジニ不純度は最頻出クラスを専用の枝に分離する傾向があるのに対してエントロピーはそれよりわずかに平衡の取れた木を作る」とあった。

両方試した所、同じ様な予測値だったのでデフォルトの gini にしている。

random_state

複数回最適なハイパーパラメータを検索したので結果を比べることが出来るように固定値を指定した。

検索結果

何回か実行して試した所、以下のハイパーパラメータを採用することにした。

最適パラメータ:{'max_depth': 10, 'min_samples_split': 3, 'n_estimators': 100}
score =  0.8603351955307262

尚、検証データでのスコアが 0.86 とまずまずの数値だが実際のテストデータでこの予測値が出るわけでない。

titanic_rft_main.py

上記で検索された最適なハイパーパラメータを指定してランダムフォレストで学習する。

# -*- coding: utf-8 -*-
"""
Created on Tue Sep 28 14:59:31 2021

・Kagle タイタニック生存者予測(学習)
・ランダムフォレスト
 
@author: Souichirou Kikuchi
"""

# GDRIVE = '/content/drive/MyDrive/M2B/Program/Kaggle/Titanic' # Google colab
GDRIVE = '.' # Local Python
VERSION = 'v006'

import sys # 自作モジュールへのPathを追加する(colabで必要)
ROOT_PATH = GDRIVE
sys.path.append(ROOT_PATH)

from rf_data import rf_data # データ前処理
from sklearn.ensemble import RandomForestClassifier
import pandas as pd

# main
X_train, X_vali, y_train, y_vali, test, test_id = rf_data(drive = GDRIVE, r_state = False) # データ加工(特徴量抽出)

model = RandomForestClassifier(max_depth = 10,
                               min_samples_split = 3,
                               n_estimators = 100,
                               criterion='gini',
                               random_state=4,
                               verbose=1)

model = model.fit(X_train, y_train)

# モデルの保存
import pickle
pickle.dump(model, open(GDRIVE + '/models/titanic_rfs_model_' + VERSION + '.pkl', 'wb'))

# モデルの復元
model = pickle.load(open(GDRIVE + '/models/titanic_rfs_model_' + VERSION + '.pkl', 'rb'))

for name, score in zip(X_train.columns, model.feature_importances_): # 特徴量の重要度
    print(name, '  {:.2f} %'.format(score*100))

# 検証用セットを用いて評価
print('score = ',model.score(X_vali, y_vali))

# テストデータを予測
results = model.predict(test)
results = results.astype('int') # Float → int
results = pd.Series(results, name = 'Survived')

# 各要素の確率を算出
probability = model.predict_proba(test)
df_proba = pd.DataFrame(probability)

# 提出データを作成
output = pd.concat([test_id, results], axis = 1)
output.to_csv(GDRIVE + '/output/RandomForest_' + VERSION + '.csv', index = False)

RandomForestClassifier

ハイパーパラメータを指定して RandomForestClassifier で学習する。

model = RandomForestClassifier(max_depth = 10,
                               min_samples_split = 3,
                               n_estimators = 100,
                               criterion='gini',
                               random_state=4,
                               verbose=1)

モデルの保存と読み込み

学習済みモデルを pickle で保存して再読み込みしている。

このスクリプトだけで言うと保存や再読み込みをする必要は無いのだが、後からバージョンを指定して特定のモデルを読み込みたい時もあるので、この様にしている。

VERSION 定数で複数のバージョンを保存できるようにした。

# モデルの保存
import pickle
pickle.dump(model, open(GDRIVE + '/models/titanic_rfs_model_' + VERSION + '.pkl', 'wb'))

# モデルの復元
model = pickle.load(open(GDRIVE + '/models/titanic_rfs_model_' + VERSION + '.pkl', 'rb'))

特徴量の重要度

ランダムフォレストでは各特徴量の重要度が可視化できる。

前述の書籍には「その特徴量を使うノードが平均して不純度をどれくらい減らすかを調べることにより、特徴量の重要度を測る」「すべての重要度の合計が1になるようにスケーリングして feature_importances_ 変数に格納する」とあった。

for name, score in zip(X_train.columns, model.feature_importances_): # 特徴量の重要度
    print(name, '  {:.2f} %'.format(score*100))

出力結果は以下の通りだ。

Sex   19.21 %
Age   10.70 %
Fare   11.97 %
Name_ttl   13.59 %
Ticket_ttl   10.98 %
Family_fare   10.12 %
Family_num   4.93 %
Pclass_1   1.86 %
Pclass_2   1.17 %
Pclass_3   5.39 %
Embarked_0   0.86 %
Embarked_1   0.77 %
Embarked_2   1.59 %
Cabin_alpha_0   0.20 %
Cabin_alpha_1   0.63 %
Cabin_alpha_2   0.36 %
Cabin_alpha_3   0.58 %
Cabin_alpha_4   0.91 %
Cabin_alpha_5   0.15 %
Cabin_alpha_6   0.16 %
Cabin_alpha_7   0.03 %
Cabin_alpha_8   3.85 %

重要度の上位は、Sex(19.21 %)、Name_ttl(13.59 %)、Fare(11.97 %)、Ticket_ttl(10.98 %)、Age(10.70 %)、Family_fare(10.12 %)、Pclass_3(5.39 %)、Family_num(4.93 %)、Cabin_alpha_8(3.85 %)だった。

Sex(性別)は事前のデータ集計でも分かっていたので納得だが Name_ttl(敬称、Mr. や Miss.)は意外に高かった。

データを Google Spreadsheet に貼り付けて並び替えたりして眺めた所、確かに miss. や master. など未成年の敬称は助かっている確率が高いので特徴量として有効なのだと思う。

ただ Age と Sex を組み合わせた項目ともとれるので、もしかしたら特徴量としては不適格なのかも知れない。

また重要度が低い項目は削除した方が予測の精度が向上する(今回は実施していない)

前述の書籍では「上記の結果に基づいた、特徴量の追加、意味のない特徴量の削除、外れ値の削除」を実施すべきとあった。

各要素の確率を計算

生存か否か(1か0か)の提出データを作成する分には不要なのだが、この後で Neutral Network での予測結果とアンサンブルで予測するためには項目毎の確率を出しておきたい。

つまり 0.51で 1 になった場合と 0.99 で 1 とでは信頼度が異なるので predict_proba で確率を算出する。

# 各要素の確率を算出
probability = model.predict_proba(test)
df_proba = pd.DataFrame(probability)

df_proba を Spyder のデバッグモードで表示している。

1行目(インデックス 0)は 0(死亡)の確率 0.911342、1(生存)の確率が 0.0886583 である事が分かる。

df_probaの中身

提出用データの作成

test データを元に学習したモデルから生存か否かの予測データを作成する。

# テストデータを予測
results = model.predict(test)
results = results.astype('int') # Float → int
results = pd.Series(results, name = 'Survived')
  ・
  ・
  ・
# 提出データを作成
output = pd.concat([test_id, results], axis = 1)
output.to_csv(GDRIVE + '/output/RandomForest_' + VERSION + '.csv', index = False)

提出結果

上記の予測結果を提出した所、結果は イマイチの 0.78468 だった。

もう少し、特徴量の抽出で試行錯誤をしないと精度は上がらない。

アンサンブル学習

ランダムフォレストと Neutral Network の2つの学習器で予測をした。

  • 0.78468:ランダムフォレストでの予測結果の正解率
  • 0.79665 :Neutral Network での予測結果の正解率

上記の2つの予測結果の行毎の確率を按分して生存結果を予測する。

コード

# -*- coding: utf-8 -*-
"""
Created on Sun Oct 17 09:07:33 2021

アンサンブル(ensemble)学習
複数のモデルの結果を統合して予測する

@author: Souichirou Kikuchi
"""

# GDRIVE = '/content/drive/MyDrive/M2B/Program/Kaggle/Titanic' # Google colab
GDRIVE = '.' # Local Python
DNN_VERSION = 'v021' # Deep Neutral Networkモデル
RFS_VERSION = 'v006' # RandomForestモデル
VERSION = 'v003' # 提出用

import sys # 自作モジュールへのPathを追加する(colabで必要)
ROOT_PATH = GDRIVE
sys.path.append(ROOT_PATH)

from rf_data import rf_data # データ前処理
from tensorflow import keras
import pandas as pd

# main()
# Deep Neutral Network
test_predict = pd.read_csv(GDRIVE + '/csv/test_predict.csv', encoding='shift-jis') # 予測用CSV(PassengerId無し)
test_pid = pd.read_csv(GDRIVE + '/csv/test_pid.csv', encoding='shift-jis') # PassengerId取得用
model = keras.models.load_model(GDRIVE + '/models/titanic_model_' + DNN_VERSION + '/main_predict_model.h5')
pre = model.predict(test_predict)
df_pre = pd.DataFrame(pre, columns=['DNN'])

# RandomForest
X_train, X_vali, y_train, y_vali, test, test_id = rf_data(drive = GDRIVE, r_state = False) # データ加工(特徴量抽出)
import pickle
model_rfs = pickle.load(open(GDRIVE + '/models/titanic_rfs_model_' + RFS_VERSION + '.pkl', 'rb'))

# 各要素の確率を算出
probability_rsf = model_rfs.predict_proba(test)
df_rfs = pd.DataFrame(probability_rsf, columns=['0','1'])
df_pre['RandomForest'] = df_rfs['1'] # 1の確率を取り出し

df_pre['Avarage'] = df_pre.mean(axis='columns') # 行ごとの平均
df_pre.loc[df_pre['Avarage'] >= 0.5, 'Survived'] = 1 # 0.5をしきい値
df_pre.loc[df_pre['Avarage'] < 0.5, 'Survived'] = 0

# 提出データを作成
output = pd.concat([test_id, df_pre['Survived'].astype('int')], axis = 1)
output.to_csv(GDRIVE + '/output/ensemble_' + VERSION + '.csv', index = False)

Neutral Network

以前に学習して保存したモデルを呼び出してテストデータで生存率を予測する。

model = keras.models.load_model(GDRIVE + '/models/titanic_model_' + DNN_VERSION + '/main_predict_model.h5')
pre = model.predict(test_predict)
df_pre = pd.DataFrame(pre, columns=['DNN'])

df_pre には行ごとの生存率が計算されて格納される。

1行目(インデックス 0)の生存率は 0.102216125 と低い。

前述のランダムフォレストでの予測でも生存率は 0.0886583 と低かったので、1行目の方は残念ながら亡くなられた可能性が高いと予測される。

Neutral Networkでの生存率の予測

ランダムフォレスト

ランダムフォレストも同様に保存したモデルを呼び出して生存率を予測する。

# 各要素の確率を算出
probability_rsf = model_rfs.predict_proba(test)
df_rfs = pd.DataFrame(probability_rsf, columns=['0','1'])
df_pre['RandomForest'] = df_rfs['1'] # 1の確率を取り出し

結果の集計

行ごとの平均を取得して 0.5 をしきい値として生存か否かの提出用データを作成する。

df_pre['Avarage'] = df_pre.mean(axis='columns') # 行ごとの平均
df_pre.loc[df_pre['Avarage'] >= 0.5, 'Survived'] = 1 # 0.5をしきい値
df_pre.loc[df_pre['Avarage'] < 0.5, 'Survived'] = 0

# 提出データを作成
output = pd.concat([test_id, df_pre['Survived'].astype('int')], axis = 1)
output.to_csv(GDRIVE + '/output/ensemble_' + VERSION + '.csv', index = False)

提出結果

上記のデータを提出した所、0.8014 だった。

まだまだやれる事はあると思うがとりあえず 80% を超えたので良かった。

アンサンブル学習の提出結果

まとめ

ランダムフォレストの予測結果(イマイチの精度)と Neutral Network での予測確率を按分した所、わずかながら精度が向上した事が分かった。

それぞれの学習器で更に精度を高めればもう少し数値は向上するはずだ。

以上で今回の記事は終了とする。

最後に

この記事が何処かで誰かの役に立つことを願っている。

尚、当記事中の商品へのリンクはAmazonアソシエイトへのリンクが含まれています。Amazonのアソシエイトとして、当メディアは適格販売により収入を得ていますのでご了承ください。

souichirou

やった事を忘れない為の備忘録 同じような事をやりたい人の参考になればと思ってブログにしてます。 主にレゴ、AWS(Amazon Web Services)、WordPress、Deep Learning、RaspberryPiに関するブログを書いています。 仕事では工場に協働ロボットの導入や中小企業へのAI/IoT導入のアドバイザーをやっています。 2019年7月にJDLA(一般社団法人 日本デイープラーニング協会)Deep Learning for GENERALに合格しました。 質問は記事一番下にあるコメントかメニュー上部の問い合わせからお願いします。

質問やコメントや励ましの言葉などを残す

名前、メール、サイト欄は任意です。
またメールアドレスは公開されません。