Effective Python(第2版) 1章を読み込んだ
Challeng-Every-Monthというコミュニティで、みんなで Effective Python を読み合うイベントに参加しています。
その学びをまとめたいと思います。
なお、Effective Pythonの .py コードがGitHubに公開されています。
また、jupyter でコードを確認したい場合は、まだ1章だけですがこちらに公開しましたのでご利用ください。
わかったこと
- 色んな書籍で、インデントの強制といった可読性を向上させるための仕組みが python ではルール化されていることは知っていましたが、公式テキストの中でこのように考え方が述べられているのは初めて知りました。
また、下記のコマンドを入力すれば python の考え方が出力されるとは徹底されていますね。
# The zen of python の表示 import this
- 項目04よりprint() 内での変数の表示
- "%s/d/f", 変数
- "{0}".format(変数)
- f "{変数}"
これらの表現があることは知っていましたが、可読性の向上のためにバージョンを追うごとに追加された仕組みであることを初めて知りました。
これからは可読性を意識して f "{変数}" を使用していきたいと思います。
- 項目07よりyield
yieldについては単語が触れられているだけですが、
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
- 項目07、08よりイテレータ
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より追加された変数に値を割り当てる新しい構文。
例えば下記のような構文があるとします。
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()の近くにあるので可視性が向上しました。
ちなみにウォルラスとはセイウチのことだそうです。
わからなかったこと
- 項目5 ヘルパー関数
ヘルパー関数がコードの中でどういう挙動をするものなのかわかりませんでした・・・。
分かり次第追記したいと思います。
以上になります、最後までお読みいただきありがとうございました。
julia と python の処理スピードを競わせてみた
データサイエンス向きの新しい言語、julia を kaggle のタイタニックのデータセットで回して、python とどちらが処理速度が速いか、比較してみました。
コードはこちらになります。
なお、01 python.ipynb が python のコード、02 julia.ipynb がjulia がコードになっております。
タイタニックのコンペそのものの解説はこちらの記事をご参照ください。
なお、今回のコードは予測精度の向上は目指しておりませんことをご了承ください。
python、julia 共通のコードの流れ
csvデータをDataFrame型で読込む
統計量の表示
欠損値の処理
- trainデータの Age の欠損値を含む行を削除
- test データの Age、Fare の欠損値を平均値に置き換え
特徴量の選定
- 今回はラベルエンコーディングのやり方がまだわからないので数値データの Pclass、Age、SibSp、Parch、Fare の5つのみを特徴量にしています。
データの分割(ホールドアウト法)
学習モデルの作成 ‐ 誤差 ‐ 予測データの作成
- ランダムフォレストを採用しました。
csvの出力
julia と python のコードの比較(抜粋)
csvデータをDataFrame型で読込む
julia は python と違いフォルダのパスを設定しなくてもカレントディレクトリを読み込めるようです。
train = pd.read_csv(csvのあるフォルダのパス + "train.csv")
- julia
train = DataFrame(load("train.csv"))
統計量の表示
train.describe()
- julia
describe(train)
julia と python では 統計量の表示の並びが違うようですね。
また、julia は デフォルトだと jupyter は 10行ほどしか表示してくれないため、設定を変更する必要があります。
下記のサイトを参考に変更してみましょう。
Windows の場合は、kernel.json は C:\ユーザー\AppData\Roaming\jupyter\kernels\julia-x.x にありました。
欠損値の処理
欠損値のカウント
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 が参考になります。
学習モデルの作成 ‐ 誤差 ‐ 予測データの作成
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 の方が高速化するかもしれません。
今後も色々試してみたいと思います。
以上になります、最後までお読みいただきありがとうございました。
英語の自然言語処理を学んだのでまとめてみた
こちらの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)
データの中身
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
何語なのか判定
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)
記号 ( ' や ! や その他諸々)の入ったテキストの確認
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())
参考記事
記号を削除します。
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())
トークン化
# 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 等のあまり文意のない単語が削除されていることがわかります。
参考記事
語幹の抽出
例: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 になったりもしていますが。。
参考記事
見出し語化
単語を、辞書に載っている形に従って分類すること
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 に変化しています。
参考記事
特徴量の作成
# 単語カウント数 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))
感情分析
TextBlob().sentiment.polarityでテキストの感情を数値化できる
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))
アノテーション
事前にコマンドラインで
python -m spacy download en_core_web_lg
を入力し、ダウンロードしましょう、install後は開発環境を立ち上げなおしましょう。
参考記事
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))
固有表現認識とは、テキストに出現する人名や地名などの固有名詞や、日付や時間などの数値表現を認識する技術のこと
参考記事
ワードクラウド
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()
参考記事
単語ベクタライズ word embeddings
単語埋め込み (Word embeddings)
単語埋め込みを使うと、似たような単語が似たようにエンコードされる、効率的で密な表現が得られます。
nlp = gensim_api.load("glove-wiki-gigaword-300")
実行すると376.1MBのデータのダウンロードが始まる
glove-wiki-gigaword-300
Wikipedia 2014 + Gigaword 5 (6B tokens, uncased)
サイズ300の単語のベクトル表現を取得するための教師なし学習アルゴリズム
このオブジェクトを使用して、単語をベクトルにマッピングできる
### 単語を選択 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個の最も類似した単語を見つけます。
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 は、高次元データを視覚化するためのツール
## グラフ 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')
plt.scatter(x=0, y=0, c="red") plt.annotate(word, xy=(0,0), xytext=(5,2), textcoords='offset points', ha='right', va='bottom')
.annotate() は矢印の描画、名称を追記
参考記事
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()
参考記事
以上になります、最後までお読みいただきありがとうございました。
強化学習の多腕バンディット問題を学んだのでまとめてみた
強化学習の勉強のために、ITエンジニアのための強化学習理論入門を読みました。
詳細な理論解説あり、コードありとかなり読み応えのある内容でした。
その中の1章、多腕バンディットのコーディングをまとめたいと思います。
なお、バンディットはスロットマシーン、腕(アーム)はスロットを回すためのアームのことで、たくさんスロットマシーンを回すシミュレーション、といったところでしょうか。
コーディング
ブログで使ったコードはこちら
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))
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))
初期値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
ACアダプター
J48のところにジャンパーピンを刺さないと、ACアダプターを差し込んでも電源が入りませんのでお気を付けください。
User Guide より
参考サイト
私の場合はジャンパーピンが同梱されていましたが、同梱されていない場合もあるようです。
また、スイッチがないためJetson Nanoの電源オフ後、コンセントを差し直さないと再びJetson Nanoを起動することができません。
Jetson Nano に電源スイッチを追加するか、スイッチ付きの OA タップを使うとよいかと思います。
(私は スイッチ付きのOAタップを使いました。)
マイクロUSBからも給電できますが、GPUやマウス、キーボード等を使うとマイクロUSBでは電力が足りなくなるようです。
参考サイト
ケース
Jetson Nano ケース冷却ファンとカメラホルダー付きA02 / B01に対応
必須ではありませんが、Jetson Nano 本体だけですと基盤むき出しなのでケースを買いました。
組立式のアクリルケースを買ったのですが、間隔がキツキツでうまく組み立てられなかったので、こちらの金属ケースを追加で買いました。
電源ボタンも同梱されているのですが、組み立て方がうまくなかったのか、ボタンを押しっぱなしにしないと電源が切れてしまうため装着しませんでした。
カメラモジュール用のホルダーもついていますが、USBカメラを買ったので使いませんでした。
Wifi
USB の Wifi でもモノによってはドライバのインストールなしで使用できるようです。
が、私が検索した時には、各ブログで紹介されている、ドライバなしで使えるUSB Wifi は、すでに販売中止になっているものが多かったです。
このWifi カードは現行販売しておりますし、ドライバなしで使えるそうなので、こちらにしました。
設置し方はこちらのサイトも参考になりました。
NVIDIA Jetson Nanoの初期設定 | Wi-Fi設定 - ロボステーション
また、アンテナを付けなくてもWifi に接続できますが、受信感度をあげるためにもアンテナがあった方が良いでしょう。
カメラ
ラズパイのカメラモジュールも使えるようですが、私は接続しても使えませんでしたが、USBカメラでも使えました。
マイクロ SD カード
Jetson Nano単体では起動しません。
起動用のイメージをマイクロ SD カードにインストールして Jetson Nano に差し込む必要があります。
イメージのダウンロードは公式ガイドを参考にしましょう。
マイクロ SD カードのフォーマット、イメージの書き込み等は専用ソフトを使います。
こちらのサイトも参考になりました。
なお、Ubuntu (Jetson Nano の OS)のパスワードを忘れた場合、イメージを更新しなおせばリセットすることが可能です。
イメージをインストールしたマイクロ SD カードは、再フォーマットすることができなくなるようですが、イメージを改めて書きこむことは可能です。
マウス、キーボード
起動後に必要になるので用意しておきましょう。
完成
完成したのがこちらです。
図らずも電源ボタン/リセットボタン用の穴がちょうど目、SDカードスロットが口に見えますね。
あふれ出る既視感・・・
まだ起動させるところまでしかできていませんが、物体検出や画像分類にもチャレンジしていこうと思います。
以上になります、最後までお読みいただきありがとうございました。
Titanicのデータを使ってPytorchを試してみた
(9/23 修正)NetクラスのNNの数を圧縮
評価基準を正解率、適合率、再現率、f1値に修正
タイタニックのデータを使ってPyTorchの実装をしてみました。
なお、PyTorchのコードは、こちらのサイトから拝借させていただきました、ありがとうございます。
本ブログのコードはこちらからダウンロードできます。
タイタニックのデータについてはkaggleのタイタニックコンペを参照するとよいかと思います。
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)
# 特徴量を適当に指定 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を比較してみた
時系列解析に使えるライブラリ、sktimeが公開されていましたので、同じく時系列解析のライブラリ、Prophetと比較してみました。
学習に使ったデータは気象庁の気温のデータです。
なお、ブログのコードはこちらになります。
(気象庁のデータも入れています)
なお、気象庁のデータをご自身で取得する場合は、下記のProphetの記事を参照されるといいでしょう。
なお、Prophet のインストールはconda、sktime のインストールは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())
# グラフ表示 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()
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は滑らかな予測となっています。
決定係数で精度を評価してみましょう。
最も当てはまりの良い場合、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