koji/メガネ男の日誌

日々の学び、活動状況を記録します。仕事のことは少なめ。

Effective Python(第2版) 1章を読み込んだ

f:id:kj_man666:20200810095316j:plain

Challeng-Every-Monthというコミュニティで、みんなで Effective Python を読み合うイベントに参加しています。

その学びをまとめたいと思います。

なお、Effective Pythonの .py コードがGitHubに公開されています。

また、jupyter でコードを確認したい場合は、まだ1章だけですがこちらに公開しましたのでご利用ください。

わかったこと

 - 色んな書籍で、インデントの強制といった可読性を向上させるための仕組みが python ではルール化されていることは知っていましたが、公式テキストの中でこのように考え方が述べられているのは初めて知りました。

 また、下記のコマンドを入力すれば python の考え方が出力されるとは徹底されていますね。

# The zen of python の表示
import this

qiita.com

  • 項目04よりprint() 内での変数の表示

 - "%s/d/f", 変数

www.javadrive.jp

 - "{0}".format(変数)

 - f "{変数}"

 これらの表現があることは知っていましたが、可読性の向上のためにバージョンを追うごとに追加された仕組みであることを初めて知りました。

 これからは可読性を意識して f "{変数}" を使用していきたいと思います。

  • 項目07よりyield

yieldについては単語が触れられているだけですが、

ailaby.com

def func():
    a = 10
    b = 20
    c = a + b
    yield c      # ここで一旦停止
 
    a = 100
    b = 200
    c = a + b
    yield c      # ここで一旦停止
 
    a = 1000
    b = 2000
    c = a + b
    yield c      # ここで一旦停止
 
 
for x in func():
    print (x)

出力 : 30 300 3000

ちなみにreturnで同じことをやろうとするとエラーになります。

def func():
    a = 10
    b = 20
    c = a + b
    return c    
 
    a = 100
    b = 200
    c = a + b
    return c    

    a = 1000
    b = 2000
    c = a + b
    return c  
  
for x in func():
    print (x)

出力 : TypeError: 'int' object is not iterable

qiita.com

for i in range(len(values_list)):
    list_2[i]
for index_num, values in enumerate(list):
for i, j in zip(values_list, index_num_list):

こちらもこれらの表現があることは知っていましたが、可読性の向上のためにバージョンを追うごとに追加された仕組みであることを初めて知りました。

可読性を意識して for ~ in zip(A, B) を使用していきたいと思います。

  • 項目09よりループ後のelse の挙動 (非推奨)

while のようなループ後にelse文を設定すると、ループ終了後にelse文が実行されます。

j = 0
while j < 5:
    j = j + 1
    print(j)
else:
    print("finished!")

出力 : 1 2 3 4 5 finish!

しかし breakが作動すると、else文は実行されないようです。

j = 0
while j < 5:
    j = j + 1
    print(j)
    if j > 3:
        break
else:
    print("finished!")

出力 : 1 2 3 4

breakも途中で終了させる機能なのに、なぜかelse文は実行されず、ピンときませんね。

そのため挙動がわかりにくく非推奨、とEffective Pythonには記載されています。

  • 項目10より := (ウォルラス演算子

python バージョン3.8より追加された変数に値を割り当てる新しい構文。

www.lifewithpython.com

例えば下記のような構文があるとします。

text = "Happy New Year!"
if len(text) < 20:
    print(f"{text} is {len(text)} word count! {len(text)} is lucky number!" )
else:
    print("stack over!")

出力 : Happy New Year! is 15 word count !

len(text)を何回も打ち込んでいるので、可読性が下がってしまいます。

そこで次のように i = len(text) としてみてはどうでしょうか。

i=len(text)

if i < 20:
    print(f"{text} is {i} word count ! {i} is lucky number!" )
else:
    print("stack over!")

出力 : Happy New Year! is 15 word count !

print()内はスッキリしましたが、i=len(text) と print() 内の間にたくさんコードが書かれてしまうと、 あれ、この i どこで定義したっけ・・・?となる可能性があります。

そこで次のようにするとどうでしょうか。

if (i=len(text)) < 20:
    print(f"{text} is {i} word count! {i} is lucky number!" )
else:
    print("stack over!")

出力 : SyntaxError: invalid syntax

エラーになってしまいます。

python は if 文の条件内で変数の代入ができない仕様なのでした。

これを解決したのが := (ウォルラス)演算子です。

if (i:=len(text)) < 20:
    print(f"{text} is {i} word count ! {i} is lucky number!" )
else:
    print("stack over!")

出力 : Happy New Year! is 15 word count !

無事エラーが出ませんし、i の定義が i を使うprint()の近くにあるので可視性が向上しました。

ちなみにウォルラスとはセイウチのことだそうです。

f:id:kj_man666:20200810093108j:plain

わからなかったこと
  • 項目5 ヘルパー関数

www.ne.jp

ヘルパー関数がコードの中でどういう挙動をするものなのかわかりませんでした・・・。

分かり次第追記したいと思います。

以上になります、最後までお読みいただきありがとうございました。

julia と python の処理スピードを競わせてみた

f:id:kj_man666:20200808182929j:plain データサイエンス向きの新しい言語、julia を kaggle のタイタニックのデータセットで回して、python とどちらが処理速度が速いか、比較してみました。

コードはこちらになります。

なお、01 python.ipynbpython のコード、02 julia.ipynb がjulia がコードになっております。

タイタニックのデータセットこちら

タイタニックのコンペそのものの解説はこちらの記事をご参照ください。

なお、今回のコードは予測精度の向上は目指しておりませんことをご了承ください。

python、julia 共通のコードの流れ

csvデータをDataFrame型で読込む

統計量の表示

欠損値の処理

 - trainデータの Age の欠損値を含む行を削除

 - test データの Age、Fare の欠損値を平均値に置き換え

相関係数

特徴量の選定

 - 今回はラベルエンコーディングのやり方がまだわからないので数値データの PclassAgeSibSpParchFare の5つのみを特徴量にしています。

データの分割(ホールドアウト法)

学習モデルの作成 ‐ 誤差 ‐ 予測データの作成

 - ランダムフォレストを採用しました。

csvの出力

julia と python のコードの比較(抜粋)

csvデータをDataFrame型で読込む

julia は python と違いフォルダのパスを設定しなくてもカレントディレクトリを読み込めるようです。

train = pd.read_csv(csvのあるフォルダのパス + "train.csv")
  • julia
train = DataFrame(load("train.csv"))
統計量の表示
train.describe()

f:id:kj_man666:20200808180639p:plain

  • julia
describe(train)

f:id:kj_man666:20200808180652p:plain

julia と python では 統計量の表示の並びが違うようですね。

また、julia は デフォルトだと jupyter は 10行ほどしか表示してくれないため、設定を変更する必要があります。

下記のサイトを参考に変更してみましょう。

Windows の場合は、kernel.json は C:\ユーザー\AppData\Roaming\jupyter\kernels\julia-x.x にありました。

ki-chi.jp

欠損値の処理

欠損値のカウント

train.isnull().sum()
  • julia
Dict(zip(names(train), sum.(eachcol(ismissing.(train)))))

欠損値を含む行を削除

train = train.dropna(subset=["Age"])
  • julia
train = train[ismissing.(train)[:, :"Age"] .== 0, :]

欠損値を他の値で置き換え

test["Age"] = test["Age"].fillna(test["Age"].mean())
  • julia
test[:, :Age] = coalesce.(test[:, :Age], mean(test[ismissing.(test)[:, :Age] .== 0, :][:, :Age]))

julia の場合は 欠損値処理のための関数があまりないのか、少し複雑なコードになっています。

相関係数を表示
train.corr()["Survived"]
  • julia
[cor(train[:, i], train[:, "Survived"]) for i in ["PassengerId", "Survived", "Pclass", "Age", "SibSp", "Parch", "Fare"]]

julia の場合は DataFrame 全体の相関係数を表示できず、for 文等を使ってすべてのカラムの相関係数を表示する必要があります。

特徴量の選定

x が説明変数、y が目的変数になっています。

x = train[["Pclass", "Age", "SibSp", "Parch", "Fare"]]

y = train["Survived"]
  • julia
x = Matrix(train[: , [:Pclass, :Age, :SibSp, :Parch, :Fare]])

y = train[:, :Survived]

julia の機械学習ライブラリはDataFrame 型のデータを読込めないようなので、Matrix() で Array型に変換する必要があります。

データの分割(ホールドアウト法)
from sklearn.model_selection import train_test_split

x_train, x_test, y_train, y_test = train_test_split(x, y, test_size=0.3, random_state=0)
  • julia
using ScikitLearn.CrossValidation: train_test_split

x_train, x_test, y_train, y_test = train_test_split(x, y, test_size=0.3, random_state=0);

julia に scikit-learn をインストールするには、こちらの GitHub が参考になります。

github.com

学習モデルの作成 ‐ 誤差 ‐ 予測データの作成
from sklearn.ensemble import RandomForestClassifier as classifier

clf = classifier(n_estimators = 100, random_state=0)

clf.fit(x_train, y_train)

y_pred = clf.predict(x_test)
  • julia
@sk_import ensemble: RandomForestClassifier

forest = RandomForestClassifier(n_estimators=100, random_state=0)

fit!(forest, x_train, y_train)

y_pred = predict(forest, x_test)

この辺はおおむね同じですね。

csvの出力
submission.to_csv('submission_rf.csv', index=False)
  • julia
submission |> CSV.write("submission.csv", delim=',' , writeheader=true)
経過時間の計測
# 処理前の時間を取得
import time
start = time.time()

# 処理後の経過時間を表示
elapsed_time = time.time() - start
print(elapsed_time)
  • julia
# 処理前の時間を取得
using Dates
start = Dates.now()

# 処理後の経過時間を表示
elapsed_time = Dates.now() - start
println(elapsed_time)

気になる処理速度を比較

  • python の処理時間 (秒)
0.7730164527893066
  • julia の処理時間 (ミリ秒 = 1/1000秒)
30270 milliseconds

なんと、julia は最初に最適化処理を行うせいか、圧倒的に python が早い結果となりました。

最適化後は julia は高速化するようなので、同じ処理を複数回行うような場合は julia の方が高速化するかもしれません。

今後も色々試してみたいと思います。

以上になります、最後までお読みいただきありがとうございました。

英語の自然言語処理を学んだのでまとめてみた

f:id:kj_man666:20200802115532j:plain

こちらのNLPの記事(英語)が勉強になりました。

Text Analysis & Feature Engineering with NLP

勉強を兼ねてkaggleのNPLコンペ、Real or Not? NLP with Disaster Tweets のデータセットを使って自然言語処理をやってみました。

本記事から学べるのは、英語のNLPにおける、言語の判定、正規表現を使った前処理、トークン化、ストップワード、語幹の抽出、見出し語化、単語・文字・文章のカウントによる特徴量の作成、感情分析、アノテーション、ワードクラウド、word embeddings、LDAになります。

コードはこちらになります。

ライブラリのインポート

import pandas as pd
import matplotlib.pyplot as plt
import seaborn as sns
import os
import re
from IPython.display import display

pd.set_option('max_columns', 500)
pd.set_option('max_rows', 500)

import warnings
warnings.filterwarnings('ignore')
使用されている言語を検出するライブラリ
import langdetect 
NLTK(Natural Language Toolkit)という、英語の自然言語処理のライブラリ
import nltk
TextBlob().sentiment.polarityでテキストの感情を数値化できる
from textblob import TextBlob
spaCyは自然言語用のライブラリ
import spacy
WordCloud用のライブラリ
import wordcloud
データセット/モデルのダウンロード、情報の取得、およびロードのためのAPI
import gensim.downloader as gensim_api
t-SNE は、高次元データを視覚化するためのツール
from sklearn import manifold
主にテキスト解析を対象としたスケーラブルな機械学習ライブラリで、Word2VecやDoc2VecをシンプルなAPIで利用することができる。
import gensim
データの読み込み

こちらからtrain.csv と test.csv をダウンロードしましょう。

path = os.getcwd() + "/"

# trainデータとtestデータの読込
train = pd.read_csv(path + "train.csv")
test  = pd.read_csv(path + "test.csv")

print("train")
display(train.head(3))
display(train.tail(3))
display(train.shape)

print("test")
display(test.head(3))
display(test.tail(3))
display(test.shape)
データの中身

f:id:kj_man666:20200802091531p:plain

keyword カラムとlocation カラムのボリュームを確かめる
# keyword カラムとlocation カラムのボリュームを確かめる
print("train data  keyword :{0}, location :{1}".format(train["keyword"].unique().shape[0], train["location"].unique().shape[0]))
print("test data  keyword :{0}, location :{1}".format(test["keyword"].unique().shape[0], test["location"].unique().shape[0]))

train data keyword :222, location :3342 test data keyword :222, location :1603

location は数が多すぎるのでカットします。

train = train.drop("location", axis=1)
test = test.drop("location", axis=1)

keyword の欠損値をカウント

print(train["keyword"].isnull().sum())
print(test["keyword"].isnull().sum())

61 26

欠損値を削除します。

train = train.dropna(axis=0)
test = test.dropna(axis=0)
keywordの分布
fig, axes = plt.subplots(nrows=1, ncols=2, figsize=(16, 10))

train["keyword"].reset_index().groupby("keyword").count().sort_values(by="index")[:20].plot(ax=axes[0], kind="barh", title='Train', legend=False)
test["keyword"].reset_index().groupby("keyword").count().sort_values(by="index")[:20].plot(ax=axes[1], kind="barh", titl

f:id:kj_man666:20200802095640p:plain

何語なのか判定

train['lang'] = train["text"].apply(lambda x: langdetect.detect(x) if 
                                 x.strip() != "" else "")

test['lang'] = train["text"].apply(lambda x: langdetect.detect(x) if 
                                 x.strip() != "" else "")

display(train.head())
display(test.head)

f:id:kj_man666:20200802100131p:plain

記号 ( ' や ! や その他諸々)の入ったテキストの確認

print("train")
display(train[train["text"].str.contains(r'[^\s\w]')].head(3))
display(train[train["text"].str.contains(r'[^\s\w]')].tail(3))
display(train[train["text"].str.contains(r'[^\s\w]')]["text"].count().sum())

print("test")
display(test[test["text"].str.contains(r'[^\s\w]')].head(3))
display(test[test["text"].str.contains(r'[^\s\w]')].tail(3))
display(test[test["text"].str.contains(r'[^\s\w]')]["text"].count().sum())

f:id:kj_man666:20200802100931p:plain

参考記事

qiita.com

記号を削除します。

train['text_clean'] = train["text"].apply(lambda x: re.sub(r'[^\w\s]',
                                                     '', x).lower().strip())

test['text_clean'] = test["text"].apply(lambda x: re.sub(r'[^\w\s]',
                                                     '', x).lower().strip())

display(train.head())
display(test.head())

f:id:kj_man666:20200802101147p:plain

トークン化

# text カラムを配列に変更
txt_train = train["text_clean"].values.tolist()
txt_test  = test["text_clean"].values.tolist()

# 単語に分解してトークン化
txt_train = [x.split() for x in txt_train]
txt_test = [x.split() for x in txt_test]

# リストの最初の要素を表示
display(txt_train[0])
display(txt_test[0])

['our', 'deeds', 'are', 'the', 'reason', 'of', 'this', 'earthquake', 'may', 'allah', 'forgive', 'us', 'all'] ['just', 'happened', 'a', 'terrible', 'car', 'crash']

ストップワード

# ストップワードリストの作成
nltk.download('stopwords')
lst_stopwords = nltk.corpus.stopwords.words("english")

# ストップワードの中身
lst_stopwords[:20]

['i', 'me', 'my', 'myself', 'we', 'our', 'ours', 'ourselves', 'you', "you're", "you've", "you'll", "you'd", 'your', 'yours', 'yourself', 'yourselves', 'he', 'him', 'his']

ストップワードの削除
for i in range(len(txt_train)):
    txt_train[i] = [word for word in txt_train[i] if word not in lst_stopwords]
    
for i in range(len(txt_test)):
    txt_test[i] = [word for word in txt_test[i] if word not in lst_stopwords]

display(txt_train[0])
display(txt_test[0])

['deeds', 'reason', 'earthquake', 'may', 'allah', 'forgive', 'us'] ['happened', 'terrible', 'car', 'crash']

削除前

['our', 'deeds', 'are', 'the', 'reason', 'of', 'this', 'earthquake', 'may', 'allah', 'forgive', 'us', 'all'] ['just', 'happened', 'a', 'terrible', 'car', 'crash']

our、are、the 等のあまり文意のない単語が削除されていることがわかります。

参考記事

blog.livedoor.jp

語幹の抽出

例:going から go、unbelievable から unbelieve を抽出

ps = nltk.stem.porter.PorterStemmer()

# 語幹の抽出
for i in range(len(txt_train)):
    txt_train[i] = [ps.stem(word) for word in txt_train[i]]
    
for i in range(len(txt_test)):
    txt_test[i] = [ps.stem(word) for word in txt_test[i]]

display(txt_train[0])
display(txt_test[0])

['deed', 'reason', 'earthquak', 'may', 'allah', 'forgiv', 'us'] ['happen', 'terribl', 'car', 'crash']

語幹抽出前

['deeds', 'reason', 'earthquake', 'may', 'allah', 'forgive', 'us'] ['happened', 'terrible', 'car', 'crash']

deeds が deedに、happened が happen に変化しています。

forgive が forgiv に、earthquake が earthquak になったりもしていますが。。

参考記事

www.haya-programming.com

見出し語化

単語を、辞書に載っている形に従って分類すること

lem = nltk.stem.wordnet.WordNetLemmatizer()
nltk.download('wordnet')

# 見出し語化
for i in range(len(txt_train)):
    txt_train[i] = [lem.lemmatize(word) for word in txt_train[i]]
    
for i in range(len(txt_test)):
    txt_test[i] = [lem.lemmatize(word) for word in txt_test[i]]

display(txt_train[0])
display(txt_test[0])

['deed', 'reason', 'earthquak', 'may', 'allah', 'forgiv', 'u'] ['happen', 'terribl', 'car', 'crash']

見出し語化前

['deed', 'reason', 'earthquak', 'may', 'allah', 'forgiv', 'us'] ['happen', 'terribl', 'car', 'crash']

us が u に変化しています。

参考記事

yottagin.com

特徴量の作成

# 単語カウント数
train['word_count'] = train["text"].apply(lambda x: len(str(x).split(" ")))

# 文字カウント数
train['char_count'] = train["text"].apply(lambda x: sum(len(word) for word in str(x).split(" ")))

# 文章カウント数
train['sentence_count'] = train["text"].apply(lambda x: len(str(x).split(".")))

# 単語の文字数(平均)
train['avg_word_length'] = train['char_count'] / train['word_count']

# 文章の単語数(平均)
train['avg_sentence_lenght'] = train['word_count'] / train['sentence_count']


# 単語カウント数
test['word_count'] = test["text"].apply(lambda x: len(str(x).split(" ")))

# 文字カウント数
test['char_count'] = test["text"].apply(lambda x: sum(len(word) for word in str(x).split(" ")))

# 文章カウント数
test['sentence_count'] = test["text"].apply(lambda x: len(str(x).split(".")))

# 単語の文字数(平均)
test['avg_word_length'] = test['char_count'] / test['word_count']

# 文章の単語数(平均)
test['avg_sentence_lenght'] = test['word_count'] / test['sentence_count']

display(train["text"][0])
display(train.head(1))
display(test["text"][0])
display(test.head(1))

f:id:kj_man666:20200802104718p:plain

感情分析

TextBlob().sentiment.polarityでテキストの感情を数値化できる




textblob.readthedocs.io

train["sentiment"] = train["text"].apply(lambda x: TextBlob(x).sentiment.polarity)
test["sentiment"] = train["text"].apply(lambda x: TextBlob(x).sentiment.polarity)

display(train.head(3))
display(test.head(3))

f:id:kj_man666:20200802105306p:plain

アノテーション

事前にコマンドライン

python -m spacy download en_core_web_lg

を入力し、ダウンロードしましょう、install後は開発環境を立ち上げなおしましょう。

参考記事

stackoverflow.com

nlp = spacy.load("en_core_web_lg")

# 固有表現認識
train["tags"] = train["text"].apply(lambda x: [(tag.text, tag.label_) 
                                for tag in nlp(x).ents])

test["tags"] = test["text"].apply(lambda x: [(tag.text, tag.label_) 
                                for tag in nlp(x).ents])

display(train.head(3))
display(test.head(3))

固有表現認識とは、テキストに出現する人名や地名などの固有名詞や、日付や時間などの数値表現を認識する技術のこと

hironsan.hatenablog.com

参考記事

qiita.com

ワードクラウド

corpus = train["text_clean"]

wc = wordcloud.WordCloud(background_color='black', max_words=100, 
                         max_font_size=35)
wc = wc.generate(str(corpus))

fig = plt.figure(figsize=(12.0, 8.0), num=1)
plt.axis('off')
plt.imshow(wc, cmap=None)
plt.show()

f:id:kj_man666:20200802110241p:plain

参考記事

amueller.github.io

単語ベクタライズ word embeddings

単語埋め込み (Word embeddings)

単語埋め込みを使うと、似たような単語が似たようにエンコードされる、効率的で密な表現が得られます。

www.tensorflow.org

nlp = gensim_api.load("glove-wiki-gigaword-300")

実行すると376.1MBのデータのダウンロードが始まる

glove-wiki-gigaword-300

Wikipedia 2014 + Gigaword 5 (6B tokens, uncased)

サイズ300の単語のベクトル表現を取得するための教師なし学習アルゴリズム

このオブジェクトを使用して、単語をベクトルにマッピングできる

github.com

### 単語を選択
word = "dance"

labels, X, x, y = [], [], [], []

for t in nlp.most_similar(word, topn=20):
    X.append(nlp[t[0]])
    labels.append(t[0])

gensim.models.Word2Vec.most_similar() は上位N個の最も類似した単語を見つけます。

tedboy.github.io

pca = manifold.TSNE(perplexity=40, n_components=2, init='pca')

new_values = pca.fit_transform(X)
for value in new_values:
    x.append(value[0])
    y.append(value[1])

t-SNE は、高次元データを視覚化するためのツール

scikit-learn.org

bunseki-train.com

## グラフ
fig = plt.figure()
for i in range(len(x)):
    plt.scatter(x[i], y[i], c="black")
    plt.annotate(labels[i], xy=(x[i],y[i]), xytext=(5,2), 
               textcoords='offset points', ha='right', va='bottom')

f:id:kj_man666:20200802111024p:plain

plt.scatter(x=0, y=0, c="red")
plt.annotate(word, xy=(0,0), xytext=(5,2), textcoords='offset points', ha='right', va='bottom')

f:id:kj_man666:20200802111811p:plain

.annotate() は矢印の描画、名称を追記

参考記事

qiita.com

LDAモデル

LDA は1つの文書が複数のトピックから成ることを仮定した言語モデルの一種

corpus = train["text_clean"]

## 前処理
lst_corpus = []

for string in corpus:
    lst_words = string.split()
    lst_grams = [" ".join(lst_words[i:i + 2]) for i in range(0, 
                     len(lst_words), 2)]
    lst_corpus.append(lst_grams)

## 単語をIDに対応付ける
id2word = gensim.corpora.Dictionary(lst_corpus)

## 単語の頻出数の作成
dic_corpus = [id2word.doc2bow(word) for word in lst_corpus] 

lda_model = gensim.models.ldamodel.LdaModel(corpus=dic_corpus, id2word=id2word, num_topics=3, 
                                            random_state=42, update_every=1, chunksize=100, 
                                            passes=10, alpha='auto', per_word_topics=True)

get_topics() は「トピック数×語彙数」の行列を返す。

lst_dics = []
for i in range(0,3):
    # get_topics() 「トピック数×語彙数」の行列を返す。
    # http://kento1109.hatenablog.com/entry/2017/12/27/114811
    lst_tuples = lda_model.get_topic_terms(i)
    
    for tupla in lst_tuples:
        lst_dics.append({"topic":i, "id":tupla[0], 
                         "word":id2word[tupla[0]], 
                         "weight":tupla[1]})
        
dtf_topics = pd.DataFrame(lst_dics, 
                         columns=['topic','id','word','weight'])

# グラフ
fig, ax = plt.subplots(figsize=(5, 10))
sns.barplot(y="word", x="weight", hue="topic", data=dtf_topics, dodge=False, ax=ax).set_title('Main Topics')
ax.set(ylabel="", xlabel="Word Importance")
plt.show()

f:id:kj_man666:20200802112518p:plain

参考記事

bit.ly

fits.hatenablog.com

以上になります、最後までお読みいただきありがとうございました。

強化学習の多腕バンディット問題を学んだのでまとめてみた

強化学習の勉強のために、ITエンジニアのための強化学習理論入門を読みました。

詳細な理論解説あり、コードありとかなり読み応えのある内容でした。

その中の1章、多腕バンディットのコーディングをまとめたいと思います。

なお、バンディットはスロットマシーン、腕(アーム)はスロットを回すためのアームのことで、たくさんスロットマシーンを回すシミュレーション、といったところでしょうか。

www.ebay.com.au

コーディング

ブログで使ったコードはこちら
github.com

import numpy as np
import pandas as pd

import warnings
warnings.filterwarnings('ignore')

# スロットの台数
slot_arms = 5

# 各スロットの期待値
# loc 平均、scale 標準偏差、size 出力配列のサイズ
e_value = np.random.normal(loc=0.0, scale=1.0, size=slot_arms)
for i in range(slot_arms):
    print("slot No.{0} 期待値: {1}".format(i, e_value[i]))

print()
print("最も期待値の大きい slot は No.{}".format(np.argmax(e_value)))

(以下、出力結果)
slot No.0 期待値: 0.39214341712590317
slot No.1 期待値: -0.20641977501211484
slot No.2 期待値: 0.6860500584941849
slot No.3 期待値: 0.4747609704743502
slot No.4 期待値: -0.7014833706044542

最も期待値の大きい slot は No.2
(以上)

沢山あるスロットの中から、もっとも期待値の大きいものを、何度も何度もランダムに選択する中で自動的に選択するというのが多腕バンディット問題のアルゴリズムです。

e_value = np.random.normal(loc=0.0, scale=1.0, size=slot_arms) により、各スロット台の期待値をランダムで設定しています。

期待値が事前にわかっていれば、単純に期待値の最も大きいスロットを選択すれば済む話ですが、多腕バンディット問題では現実のカジノ(パチンコやパチスロでもOK)と同じく、台の期待値がわかりません。

そこで、累計での結果が一番良かったものを選択し続ける、というアルゴリズムを用いて、結果的に一番期待値の高い台を抽出する、というのが多腕バンディット問題のアルゴリズムになります。

# スロットを回す回数
steps = 500

# ε(イプシロン)-greedy法の係数でスロットの中からランダムで選ぶ(=探索する)確率
epsilon = 0.1

# スロットマシーンの初期値
i_value = 0.0

# スロットマシーン台数分の初期値を格納した配列
qs = [i_value] * slot_arms

# qsの内容をkeyに記録する空の辞書型 qs_histを作成
qs_hist = {}
for i in range(slot_arms):
    qs_hist[i] = []
    
# 重み
w = 0.1

for i in range(steps):
    # epsilon は 0.1
    # ε(イプシロン)% の確率でスロットの中からランダムで選ぶ(=探索する)
    # np.random.random() で 0.0以上、1.0未満の乱数を作成
    if np.random.random() < epsilon:
        arm = np.random.randint(len(qs))
    
    else:
    # 残りの確率 (1 - ε)% は期待値の最も大きいスロットを選択
        arm = np.argmax(qs)
    
    # reward はスロットから得られる報酬
    # loc 平均、scale 標準偏差、size 出力配列のサイズ
    # 胴元が儲かる(プレイヤーが損をする)ように平均値は-0.1とした 
    reward = np.random.normal(loc=e_value[arm], scale=1.0)
    qs[arm] += w * (reward - qs[arm])

    for arm in range(slot_arms):
        qs_hist[arm].append(qs[arm])

ただし、収支がプラスだが期待値が最大でないスロット台をたまたま序盤に選択した結果、結果期待値が最大でないスロット台が選ばれる可能性もあります。

これを防ぐために、下記のε-greedy(イプシロン-グリーディ)法を用います。

# epsilon は 0.1
    if np.random.random() < epsilon:
        arm = np.random.randint(len(qs))
    
    else:
        arm = np.argmax(qs)

elseの部分で、配列の中で最も値が大きいものの位置を返しています。

これにより、その後のコードで最も大きい値のものを選択することになるわけですが、if の部分で epsilon(ε イプシロン)%の確率でランダム探索を行っています。

これにより、収支がプラスだが期待値が最大でないスロット台をたまたま序盤に選択した結果、結果期待値が最大でないスロット台の選択が続いてしまっても、選択する台の変更の余地を残すことが可能になります。

# グラフで表示
pd.DataFrame(qs_hist).plot(figsize=(6, 6), title='Initiative Value {}'.format(i_value))

f:id:kj_man666:20200724092923p:plain
初期値が0.0のグラフ

50回目くらいまでは3番目に期待値の高い No.0 が選択され続けるが、50~300回目くらいまでは最も初期値の高い No.2 の成果が大きくなりました。

これは、探索でランダムに抽出した結果、No.2を選択し、その後も選ばれ続けている結果と思われます。

また300~340回目くらいにNo.0 が選択され続けているのも、探索でランダムに抽出した結果その後も選ばれ続けたためと思われます。

# スロットマシーンの初期値を変更
i_value2 = 3.0

# スロットマシーン台数分の初期値を格納した配列を新たに作成
qs2 = [i_value2] * slot_arms

# qsの内容をkeyに記録する空の辞書型 qs_histを新たに作成
qs_hist2 = {}
for i in range(slot_arms):
    qs_hist2[i] = []
for i in range(steps):
    
    # ε(イプシロン)% の確率でスロットの中からランダムで選ぶ(=探索する)
    # np.random.random() で 0.0以上、1.0未満の乱数を作成
    if np.random.random() < epsilon:
        arm = np.random.randint(len(qs2))
    
    else:
    # 残りの確率 (1 - ε)% は期待値の最も大きいスロットを選択
        arm = np.argmax(qs2)
    
    # reward はスロットから得られる報酬
    # loc 平均、scale 標準偏差、size 出力配列のサイズ
    reward = np.random.normal(loc=e_value[arm], scale=1.0)
    qs2[arm] += w * (reward - qs2[arm])

    for arm in range(slot_arms):
        qs_hist2[arm].append(qs2[arm])

# グラフを表示
pd.DataFrame(qs_hist2).plot(figsize=(6, 6), title='Initiative Value {}'.format(i_value2))

f:id:kj_man666:20200724092844p:plain
初期値が3.0のグラフ

初期値0.0の場合との大きな違いは、序盤の130回目くらいまで、すべてのパターンが選択され続けている点です。

初期値は以下のコーディング部分に影響します。

qs2 = [i_value] * slot_arms
qs2[arm] += w * (reward - qs2[arm])

初期値0.0の場合は、qs2[arm]の初期値が3.0となります。

reward - qs2[arm] がプラスになる可能性が高く、今回選択したスロット台が実は期待値が最大でない場合でも、しばらく選択され続ける可能性があります。

一方、初期値を3.0に変更すると、reward - qs2[arm] がマイナスとなる可能性が高く、今回選択されたスロット台が次に選択されず、他のスロット台を選択できる可能性が高くなります。

これにより、たまたま最初に選択された期待値が最大でない台が連続して選択され続けることを防止することができ、期待値最大のスロット台を早い段階で検出することが可能となります。

自分がスロットを実際にしているところをイメージしてもらうと、たまたま最初に選択したスロット台の設定が実はそんなによくなくても、最初にたまたまいい結果がでてしまうと、なかなかその台を離れることができない、というのは理解できるところでしょう。

以上になります、最後までお読みいただきありがとうございました。

Jetson Nano を買ってセットアップしたのでまとめてみた

IoTを使った画像解析をやりたくてJetson Nanoを買いました。

なお、本記事のとおりに実施すればJetson Nano をセットアップできることを保証するものではありませんこと、ご了承ください。

買ったものは以下のとおりです。

Jetson Nano

本体 B01

ACアダプター

ACアダプター 5V 4A

J48のところにジャンパーピンを刺さないと、ACアダプターを差し込んでも電源が入りませんのでお気を付けください。

f:id:kj_man666:20200723142001p:plain

User Guide より

参考サイト

wazalabo.com

私の場合はジャンパーピンが同梱されていましたが、同梱されていない場合もあるようです。

また、スイッチがないためJetson Nanoの電源オフ後、コンセントを差し直さないと再びJetson Nanoを起動することができません。

Jetson Nano に電源スイッチを追加するか、スイッチ付きの OA タップを使うとよいかと思います。

(私は スイッチ付きのOAタップを使いました。)

マイクロUSBからも給電できますが、GPUやマウス、キーボード等を使うとマイクロUSBでは電力が足りなくなるようです。

参考サイト

denor.jp

ケース

Jetson Nano ケース冷却ファンとカメラホルダー付きA02 / B01に対応

必須ではありませんが、Jetson Nano 本体だけですと基盤むき出しなのでケースを買いました。

組立式のアクリルケースを買ったのですが、間隔がキツキツでうまく組み立てられなかったので、こちらの金属ケースを追加で買いました。

電源ボタンも同梱されているのですが、組み立て方がうまくなかったのか、ボタンを押しっぱなしにしないと電源が切れてしまうため装着しませんでした。

カメラモジュール用のホルダーもついていますが、USBカメラを買ったので使いませんでした。

Wifi

WiFiカード 8265NGW

アンテナ

USB の Wifi でもモノによってはドライバのインストールなしで使用できるようです。

が、私が検索した時には、各ブログで紹介されている、ドライバなしで使えるUSB Wifi は、すでに販売中止になっているものが多かったです。

このWifi カードは現行販売しておりますし、ドライバなしで使えるそうなので、こちらにしました。

www.neko.ne.jp

設置し方はこちらのサイトも参考になりました。

NVIDIA Jetson Nanoの初期設定 | Wi-Fi設定 - ロボステーション

また、アンテナを付けなくてもWifi に接続できますが、受信感度をあげるためにもアンテナがあった方が良いでしょう。

カメラ

ラズパイのカメラモジュールも使えるようですが、私は接続しても使えませんでしたが、USBカメラでも使えました。

マイクロ SD カード

Jetson Nano単体では起動しません。

起動用のイメージをマイクロ SD カードにインストールして Jetson Nano に差し込む必要があります。

イメージのダウンロードは公式ガイドを参考にしましょう。

f:id:kj_man666:20200723145454p:plain
赤枠の部分をクリックすると、イメージのインストールガイドが表示されます

developer.nvidia.com

マイクロ SD カードのフォーマット、イメージの書き込み等は専用ソフトを使います。

こちらのサイトも参考になりました。

note.com

なお、Ubuntu (Jetson Nano の OS)のパスワードを忘れた場合、イメージを更新しなおせばリセットすることが可能です。

イメージをインストールしたマイクロ SD カードは、再フォーマットすることができなくなるようですが、イメージを改めて書きこむことは可能です。

マウス、キーボード

起動後に必要になるので用意しておきましょう。

完成

完成したのがこちらです。

f:id:kj_man666:20200723140006j:plain

図らずも電源ボタン/リセットボタン用の穴がちょうど目、SDカードスロットが口に見えますね。

あふれ出る既視感・・・

www.kyd-store.jp

まだ起動させるところまでしかできていませんが、物体検出や画像分類にもチャレンジしていこうと思います。

suzukitakahiro.sakura.ne.jp

以上になります、最後までお読みいただきありがとうございました。

Titanicのデータを使ってPytorchを試してみた

f:id:kj_man666:20200712182438j:plain

(9/23 修正)NetクラスのNNの数を圧縮
       評価基準を正解率、適合率、再現率、f1値に修正

note.nkmk.me

タイタニックのデータを使ってPyTorchの実装をしてみました。

なお、PyTorchのコードは、こちらのサイトから拝借させていただきました、ありがとうございます。

rf00.hatenablog.com

本ブログのコードはこちらからダウンロードできます。

タイタニックのデータについてはkaggleのタイタニックコンペを参照するとよいかと思います。

www.kaggle.com

import pandas as pd
import numpy as np

from IPython.display import display
from sklearn.model_selection import train_test_split

from sklearn.metrics import accuracy_score # 正解率
from sklearn.metrics import precision_score # 適合率
from sklearn.metrics import recall_score # 再現率
from sklearn.metrics import f1_score # F1値

# pytorch
import torch
from torch.autograd import Variable
import torch.nn as nn
import torch.nn.functional as F
import torch.optim as optim

pd.set_option('max_columns', 500)
pd.set_option('max_rows', 500)

# タイタニックのデータの読込
url = "https://raw.githubusercontent.com/mwaskom/seaborn-data/master/raw/titanic.csv"
df = pd.read_csv(url)

display(df.head())
display(df.tail())
display(df.shape)

f:id:kj_man666:20200712182314p:plain

# 特徴量を適当に指定
feature = ["pclass", "sibsp", "parch", "fare"]

X_train, X_test, y_train, y_test = train_test_split(df[feature], df["survived"], test_size=0.3, random_state=42)

display(X_train.shape) # (623, 4)

PyTorchのデータセットは、numpy型を取り込んで、tensor型でする必要があります。

そこで、.valuesを使ってDataFrame型からnumpy.ndarray型に変換して、torch.Tesor()、LongTensor()でtensor型に変換しています。

また、説明変数をTensor()、目的変数をLongTensor()で変換しないと、RuntimeErrorが発生しますので間違えないようにしましょう。

Longというのは、PyTorchで使われる64bitの整数の型のようです。

X_train = torch.Tensor(X_train.values)
X_test = torch.Tensor(X_test.values)
y_train = torch.LongTensor(y_train.values)
y_test = torch.LongTensor(y_test.values) 

# シードを固定
torch.manual_seed(42)

ニューラルネットの関数です。

class Net(nn.Module):
    def __init__(self):
        super(Net, self).__init__()
        self.fc1 = nn.Linear(4, 20)  # (X_train.shape[1], X)
        self.fc2 = nn.Linear(20, 10) # (X, Y)
        self.fc3 = nn.Linear(10, 2)   # (Y, Z)
    def forward(self, x):
        x = F.relu(self.fc1(x))
        x = F.relu(self.fc2(x))
        x = F.relu(self.fc3(x))
        return F.log_softmax(x, dim = 1)

関数の中に設定されている数値が気になりますね。

self.fc1 = nn.Linear(4, 20)
self.fc2 = nn.Linear(20, 10)
self.fc3 = nn.Linear(10, 2)

self.fc1 の1番目の数値 X_trainの次元数 4
 (参考)X_train.shape → (623, 4)

self.fc1 の2番目の数値と、self.fc2 の1番目の数値を同じにする

self.fc2 の2番目の数値と、self.fc3 の1番目の数値を同じにする

2クラス分類なので、self.fc3 の2番目の数値を 2 にする

数学よわよわ勢なので間違っているかもしれませんが・・・行列の積ができるようにそろえているんでしょう。

model = Net()
print(model)

optimizer = optim.SGD(model.parameters(), lr=0.02)
train_loss = []
train_accu = []
i = 0

model.train() #学習モードに切り替え

for epoch in range(2000): # 数字は適当
    data, target = Variable(X_train), Variable(y_train)
    optimizer.zero_grad()
    output = model(data) 
        
    loss = F.nll_loss(output, target)
    loss.backward()
    train_loss.append(loss.data.item())
    optimizer.step()
        
    prediction = output.data.max(1)[1]
    accuracy = prediction.eq(target.data).sum().numpy() / len(X_train)
    train_accu.append(accuracy)
    
    if i % 10 == 0:
        print('Train Step: {}\tLoss: {:.3f}\tAccuracy: {:.3f}'.format(i, loss.data.item(), accuracy))
    i += 1

print('Train Step: {}\tLoss: {:.3f}\tAccuracy: {:.3f}'.format(i, loss.data.item(), accuracy))

model.eval() #推論モードに切り替え
outputs = model(Variable(X_test))
_, predicted = torch.max(outputs.data, 1)

# 誤差算定
print(f"正解率 :{accuracy_score(y_test, predicted)}")
print(f"適合率 :{precision_score(y_test, predicted)}")
print(f"再現率 :{recall_score(y_test, predicted)}")
print(f"F1スコア :{f1_score(y_test, predicted)}")

精度は以下のとおりでした。

正解率 :0.7238805970149254
適合率 :0.6907216494845361
再現率 :0.6036036036036037
F1スコア :0.6442307692307693

以上になります、最後までお読みいただきありがとうございました。

時系列解析ライブラリ sktimeとProphetを比較してみた

f:id:kj_man666:20200705192646j:plain

時系列解析に使えるライブラリ、sktimeが公開されていましたので、同じく時系列解析のライブラリ、Prophetと比較してみました。

学習に使ったデータは気象庁の気温のデータです。

なお、ブログのコードはこちらになります。

気象庁のデータも入れています)

なお、気象庁のデータをご自身で取得する場合は、下記のProphetの記事を参照されるといいでしょう。

qiita.com

なお、Prophet のインストールはcondasktime のインストールはpipを使用していますが、pipとcondaを混在させるのはよくないようですので再現する際は自己責任でお願いいたします。

内容を見てみましょう。

# ライブラリ
import matplotlib.pyplot as plt
import numpy as np
import pandas as pd
import os
import datetime

from IPython.display import display

# 時系列データの分割
from sktime.forecasting.model_selection import temporal_train_test_split

# sktime ナイーブベイズ
from sktime.forecasting.naive import NaiveForecaster

# Prophet
from fbprophet import Prophet

# 決定係数
from sklearn.metrics import r2_score

%matplotlib inline

Prophetで読み込むデータは、日付データのカラムを ds 、学習データのカラム y と指定する必要があるようですので、指定します。

# カレントディレクトリをpathに指定
path = os.getcwd()

# データの読込
df = pd.read_csv(path + "\data.csv", encoding="shift-jis", usecols=[0,1], names=["ds", "y"], header=2)
df = df.drop(index=0, axis=0)
df["ds"] = pd.to_datetime(df["ds"])

display(df.head())
display(df.tail())

f:id:kj_man666:20200705183630p:plain

# グラフ表示
fig = plt.figure(figsize=(20, 8))

plt.title("Tokyo tempreture 1990-2019")
plt.xlabel("time")
plt.ylabel("tempreture")

plt.plot(df["ds"], df["y"], label="temp")
plt.legend()
plt.show()

f:id:kj_man666:20200705184158p:plain

sktimeのライブラリ、temporal_train_test_split()を使って、テストデータの2019/1/1~2019/12/31 1年分のデータとそれ以外の訓練データに分割します。

ホールドアウト法の train_test_split() と違い、ランダム配分は行わないので、時系列データを分割するときは便利ですね!

y_train, y_test = temporal_train_test_split(df, test_size=365)

Prophet の予測データの作成

model_pro = Prophet(weekly_seasonality=True, yearly_seasonality=True, daily_seasonality=True)

# 学習
model_pro.fit(y_train)

# 予測用の365日のDataFrameの作成
fyear = model_pro.make_future_dataframe(periods=365)

sktime(ナイーブベイズ)の予測データの作成

fh = np.arange(len(y_test))+1

# モデルの作成
model_sk = NaiveForecaster(strategy="seasonal_last", sp=365)

# 学習
model_sk.fit(y_train["y"])

# 予測データの作成
forecast_sk = model_sk.predict(fh)

これでProphetとsktimeの予測データを作成できました。

グラフに表示してみましょう。

fig = plt.figure(figsize=(24, 8))

plt.plot(y_train["ds"][-731:-1], y_train["y"][-731:-1], label="y_train")
plt.plot(y_test["ds"], y_test["y"], label="y_test")
plt.plot(y_test["ds"], forecast_sk, label="sktime")
plt.plot(y_test["ds"], forecast_pro["yhat"][-366:-1], label="prophet")

plt.title("Tokyo tempreture 1990-2019")
plt.xlabel("time")
plt.ylabel("tempreture")

plt.legend()
plt.show()

実データ y_test と、予測データ sktime、prophet を見比べてみましょう。

sktimeは、日々のジグザグした変動をよく表現しています。

一方、prophetは滑らかな予測となっています。

f:id:kj_man666:20200705190755p:plain

決定係数で精度を評価してみましょう。

最も当てはまりの良い場合、1.0 となるそうです。

print("Prophet 決定係数:{}".format(r2_score(y_test["y"], forecast_pro["yhat"][-366:-1])))

print("sktime  決定係数:{}".format(r2_score(y_test["y"], forecast_sk)))

Prophet の決定係数:0.9015399553586755

sktime の決定係数:0.7408533090595817

Prophet は 日々の変動を反映していませんが、決定係数はsktimeよりも大きく精度が高いようです。

今回はProphetの方に軍配が上がりましたが、パラメーターを修整すると結果も変わりそうですね。

以上になります、最後までお読みいただきありがとうございました。

【参考サイト】 【sktime の GitHub】 この中のexamples > 01_forecasting.ipynb を参考にさせていただいております。 github.com

pythondatascience.plavox.info