pandasのデータフレームの行をランダムにシャッフルする

てっきり専用のメソッドがあると思っていたら、無さそうだったのでやり方のメモです。
pandas のデータフレームのデータをランダムにシャッフルする方法を紹介します。

これを使う場面の一例ですが、たとえばkerasで作ったモデルを学習する時にvalidation_split を使う場合、
データをシャッフルせずに後ろの方から指定した割合をvalidationデータとして使うため、
事前にデータをシャッフルしておかないとtrainとvalidationでデータの傾向が違うということが発生し得ます。
参考:validation splitはどのように実行されますか?

pandasのデータフレームをランダムに混ぜ合わせる方法は何通りも考えらるのですが、
sample()メソッドに、frac=1を渡して実行するのが一番簡単そうです。
参考:DataFrameのsampleメソッドのドキュメントを読む
そうすることによって、データフレームの全ての行がサンプリングされ、さらに、sample()の仕様により、データはシャッフルされます。

ついでにインデックスの振り直しまでやっておくと便利なので、次の例のように使うと良さそうです。


import pandas as pd
# サンプルデータ生成
df = pd.DataFrame(
    {f"col{i}": [f"value{i}{j}" for j in range(10)] for i in range(3)}
)
print(df)
"""
      col0     col1     col2
0  value00  value10  value20
1  value01  value11  value21
2  value02  value12  value22
3  value03  value13  value23
4  value04  value14  value24
5  value05  value15  value25
6  value06  value16  value26
7  value07  value17  value27
8  value08  value18  value28
9  value09  value19  value29
"""

# シャッフルしてインデックスを振り直す
df = df.sample(frac=1).reset_index(drop=True)
print(df)
"""
      col0     col1     col2
0  value08  value18  value28
1  value06  value16  value26
2  value00  value10  value20
3  value03  value13  value23
4  value05  value15  value25
5  value02  value12  value22
6  value07  value17  value27
7  value04  value14  value24
8  value09  value19  value29
9  value01  value11  value21
"""

jupyter notebook で HTMLを表示する

諸事情あって、notebookで出力するテキストに色を塗りたかったので、方法を調べました。
結論として、spanタグでbackground-colorを設定したHTMLを組み立てて、それを表示する方法をとりました。

notebookで、変数に格納されたHTMLテキストを表示するには、次の関数を使います。

class IPython.display.HTML

使い方のイメージ


from IPython.display import HTML
# 変数 html_text に表示したいhtmlが文字列で入ってるものとする。
HTML(html_text)

ただ、変数に格納されたhtmlを表示するのではなく、普通にhtmlを書くのであれば、
セルをマークダウンモードにしておけば自動的にhtmlとして解釈して表示してくれます。

この他、 %%html というマジックコマンドを使うことも可能です。
参考:cellmagic-html

kerasで使える活性化関数を可視化する

kerasではあらかじめ様々な活性化関数が用意されています。
正直普段使ってないものもあるのですが、どのようなもがあるのか一通り把握しておきたかったので可視化を試みました。
原論文か何か読んで数式を順番に実装しようかと思ったのですが、
Activationレイヤーだけから構成される超単純なモデルを作るのが手軽だと気付いたのでやってみました。

今回はこちらのページにある、 利用可能な活性化関数 を対象にしました。
(この他にもより高度な活性化関数はいくつか用意されています。)

コードは次のようになります。


from tensorflow.keras.models import Sequential
from tensorflow.keras.layers import Activation
import numpy as np
import matplotlib.pyplot as plt

x = np.linspace(-5, 5, 101).reshape(-1, 1)

# 絶対値の範囲が 有限なもの
activations_0 = [
    "sigmoid",
    "tanh",
    "hard_sigmoid",
    "softsign",
    ]

# 絶対値の範囲に上限がないもの
activations_1 = [
    "elu",
    "selu",
    "softplus",
    "relu",
    "linear",
]

fig = plt.figure(figsize=(10, 8))
for i, activations in enumerate([activations_0, activations_1]):
    ax = fig.add_subplot(2, 1,  i+1)
    for activation_str in activations:
        model = Sequential()
        model.add(
            Activation(
                activation_str,
                input_shape=(1, ),
            )
        )
        y = model.predict(x).ravel()
        ax.plot(x, y, label=activation_str)

    ax.legend()

plt.show()

出力結果はこちら。

bashコマンドで標準出力に出力する内容をファイルにも書き出す

久々に使おうとしたら忘れてしまっていたのでメモです。

長時間かかるコマンドの実行を待つ時、標準出力にずらずらと出てくるメッセージを見ながら、
それをファイルにも残しておきたいことがあります。
だいたい、リダイレクション(>や>>)を使って、ファイルに書き出しながら、
ファイルを tail -f コマンドで監視することが多いのですが、
tee という専用のコマンドも用意されています。

次のように、パイプラインで出力をtee コマンドに渡してあげて、 teeコマンドの引数に出力したいファイルを指定しておくと、
標準出力にはそのまま出力され、ファイルにも同じ内容が残ります。


$ echo 'tee Test' | tee file.txt
tee Test
$ cat file.txt
tee Test

-aをつけると追記です。(つけないと上書き)


$ echo 'tee Test2' | tee -a file.txt
tee Test2
$ cat file.txt
tee Test
tee Test2

printでお手軽プログレスバー

前回のprintのオプションの記事の応用です。
参考:Python3のprintの引数

大きめのデータの前処理など待ち時間が長い処理をする時に、進捗を表示したくなる時があります。
専用の良いライブラリもたくさんあるので、実用上はそれで十分なのですが、
前回のprintのオプションを使うと、簡易的なプログレスバーを作成できます。
(何も考えずに printしても進捗はわかるのですが、notebookが見にくくなったりします。)

ポイントは end を使って改行を止めることと、 “\r”(キャリッジリターン)を出力して、
次の文字の出力先を行の先頭にし、前回の出力を上書きすることです。

一例としてやってみます。
本当は重い処理のループなどの中でやるのですが、今回例として time の sleepで無理やり待ち時間を入れています。


import time
for i in range(1, 101):
    print("■" * (i//10), "□"*(10-i//10), sep="", end=" : ")
    print(str(i).zfill(3), "/", 100, "\r", sep="", end="")
    time.sleep(1)

手軽に、という割にコードが汚くなりました。
■と□は余計かもしれませんね。

これを実行すると、こんな感じの表示が出て数字が増えながら、徐々に黒く塗りつぶされていきます。
■■■■□□□□□□ : 040/100

Python3のprintの引数

printは表示したい文字列以外にも色々引数を指定して出力をカスタマイズできるよという話。

Python3系では、Python2と違って、printは関数です。(2では文)
そのため、色々と引数をとることができます。(文ならカスタマイズできないわけでもないし、関数が絶対引数とるわけでもないのですが、その辺は置いといて。)

あまりにも使い慣れすぎてドキュメントを読んでなかったのですが、printの説明には次のようにあります。
print(*objects, sep=’ ‘, end=’\n’, file=sys.stdout, flush=False)

objects を sep で区切りながらテキストストリーム file に表示し、最後に end を表示します。sep 、 end 、 file 、 flush を与える場合、キーワード引数として与える必要があります。

キーワードなしの引数はすべて、 str() がするように文字列に変換され、 sep で区切られながらストリームに書き出され、最後に end が続きます。 sep と end の両方とも、文字列でなければなりません。これらを None にすると、デフォルトの値が使われます。 objects が与えられなければ、 print() は end だけを書き出します。

file 引数は、 write(string) メソッドを持つオブジェクトでなければなりません。指定されないか、 None である場合、 sys.stdout が使われます。表示される引数は全てテキスト文字列に変換されますから、 print() はバイナリモードファイルオブジェクトには使用できません。代わりに file.write(…) を使ってください。

出力がバッファ化されるかどうかは通常 file で決まりますが、flush キーワード引数が真ならストリームは強制的にフラッシュされます。

バージョン 3.3 で変更: キーワード引数 flush が追加されました。

printにカンマ区切りで複数文字列を渡すと、スペースで結合して表示してくれるのは知っていたのですが、
それがsepって引数で調整できることは初めて知りました。


# デフォルトではsep=" "で区切って出力
print("abc", "def", "ghi")
# abc def ghi

# sepを指定すると区切り文字を変えられる
print("abc", "def", "ghi", sep="==>")
# abc==>def==>ghi

# 末尾につける文字もend(デフォルトは改行) で変更可能
print("abc", "def", "ghi", end="jkl")
# abc def ghijkl

これまで、いくつかの文字列を出力したいけど、途中にスペースや改行を入れたくないときは、
“+”で文字列を連結して表示文字列を作ったりしていましたが、
これでそんな面倒なことをしなくても大丈夫になりました。

numpyの高次元配列に対するdot積の挙動について

numpyの比較的よく使う関数に、dot積があります。
スカラーとベクトルを渡せばベクトルをスカラー倍してくれて、ベクトル同士なら内積を取ってくれ、
行列を二つ渡せばそれらの行列積を戻してくれるとても便利な関数です。
(本当は行列積は、 np.matmul を使ったほうがいいらしい。)

さて、そのnp.dot ですが、行列よりもより高次元の配列についても定義されていることを最近知りました。
ドキュメント : numpy.dot

二つの多次元配列$a$と$b$に対して、$a$の一番最後の次元の長さと、$b$の最後から2番目の次元の長さが等しい時に、
np.dot(a, b)を計算することができます。
i*j行列と、j*k行列に積が定義されて、その積がi*k行列になるのと似ています。

具体的な挙動について、コード動かしてみていきましょう。
まず、サンプルとなるデータを作ります。
aの最後の次元の要素数と、bの最後から2番目の次元の要素数は 5で揃えましたが、
それ以外の次元の要素数はバラバラにして、結果と比較しやすいようにしました。


import numpy as np
a = np.random.randn(2, 3, 4, 5).round(2)
b = np.random.randn(6, 7, 5, 8).round(2)

print(a.shape, b.shape)
# (2, 3, 4, 5) (6, 7, 5, 8)

このdot積を取って、shapeをみてみます。


c = np.dot(a, b)
print(c.shape)
# (2, 3, 4, 6, 7, 8)

5は消えましたが残りの数はそのまま残りましたね。

さて、結果の$c$の各要素の値ですが、次のように計算されたものが入っています。
$$
c[i, j, k, m, n, o] = \sum_{l} a[i, j, k, l] \cdot b[m, n, l, o].
$$
別の書き方をすればこうです。
$$
c[i, j, k, m, n, o] = np.dot(a[i, j, k, :], b[m, n, :, o]).
$$

一応確認しておきましょう。


c[1, 2, 3, 4, 5, 6] ==  np.dot(a[1, 2, 3, :], b[4, 5, :, 6])
# True

データフレームの列からapplyで新しい列を作る時、複数列まとめて作成する

DataFrame(その列なので正確にはSeries)に、何か関数を適用して新しい列を作ることは、
機械学習の特徴量作成や前処理などで頻繁に行う処理だと思います。

いつも、1列作るごとに、applyして結果を得ています。
例えば、とある列の値を2乗した列と、3乗した値が欲しいときは次のように書きます。


df = pd.DataFrame(
        np.random.randint(10, size=(5, 3)),
        columns=[f"col{str(i)}" for i in range(3)]
)

# 生成するデータごとにapplyする
df["pow_2"] = df["col0"].apply(lambda x: x**2)
df["pow_3"] = df["col0"].apply(lambda x: x**3)

print(df)

"""
   col0  col1  col2  pow_2  pow_3
0     0     1     6      0      0
1     9     8     8     81    729
2     1     6     8      1      1
3     5     1     2     25    125
4     0     2     5      0      0
"""

このくらい簡単な例であれば、計算負荷も大したことがないのですが、
物によっては、非常に無駄な処理をすることがあります。
例えば日本語の自然言語処理で大量のテキストを形態素解析し、表層形と原形と品詞の列を
それぞれ取得したいときなど、共通の形態素解析処理部分は一回で済ましたいので
3列個別にapplyするなどやりたくありません。

このような場合、applyする関数の戻り値をSeriesで戻せば、
applyの戻りを服す列にできることを知りました。

例えば次のように書きます。


# もう一度サンプルデータ生成
df = pd.DataFrame(
        np.random.randint(10, size=(5, 3)),
        columns=[f"col{str(i)}" for i in range(3)]
)

df[["pow_2", "pow_3"]] = df["col0"].apply(lambda x: pd.Series([x**2, x**3]))
print(df)
"""
   col0  col1  col2  pow_2  pow_3
0     6     3     1     36    216
1     8     4     9     64    512
2     4     4     7     16     64
3     1     1     4      1      1
4     1     8     3      1      1
"""

タプルや配列ではだめで、Seriesで返した場合のみの挙動です。
lambda式を遣わず、普通に定義したSeriesを返す関数でもできます。

なぜこのような挙動になるのか公式ドキュメント内からは該当箇所を探せていないのですが、
とても便利なので積極的に使っていきたいです。

KerasのTokenizerの細かい設定

前回の記事の続きで、 Keras の Tokenizer の話です。

Tokenizerのインスタンスを作成する時、前回紹介した引数の filters / lower/ split /char_level のほか、
num_wordsと、oov_token という二つの引数を設定することができます。
これが少し癖があるので確認しておきましょう。

まず、num_wordsですが、こちらは学習する単語の種類数を指定するものです。
ここで注意しないといけないのは、インデックス0がどの単語にも割り当てられないインデックスとして予約されてしまっているので、
Tokenizerが学習してくれる単語は、num_wordsで指定したのよりも一つ少なくなります。
(普通は数千とかの値を渡すのですが動きを確認しやすくするために、)試しにnum_words=7 を渡してみましょう。
なお、前回の記事同様に、 変数 text_data に、テキストデータが入ってるものとします。


# num_words で、学習する語彙数を指定できる。
keras_tokenizer_7 = Tokenizer(
                            num_words=7
                        )

# 文字列から学習する
keras_tokenizer_7.fit_on_texts(text_data)

# 学習した単語とそのindex
print(keras_tokenizer_7.word_index)
# {'the': 1, 'of': 2, 'to': 3, 'and': 4, 'a': 5, 'in': 6, 'is': 7, 'i': 8, 'that': 9, 'it': 10, 'for': 11, 'this': 12,... 

まず、戸惑うところは、word_indexには大量の単語が含まれている点です。でも、実際にトークン化できる単語は頻度が高い7-1=6個だけになります。
試しに高頻度の単語を並べたテキストをトークン化してみましょう。


print(keras_tokenizer_7.texts_to_sequences(["the of to and a in is i that it for"]))
# [[1, 2, 3, 4, 5, 6]]

ちょっとわかりにくいですが、 the から in までの 6単語が変換され、 is 以降は無視されました。

次に、 oov_token です。 これは元々のテキストに含まれていなかったり、num_wordsで指定した上限値によって溢れてしまった単語を、
oov_token で渡した文字列に変換するものです。
それだけなら単純ですが、注意しないといけないのは、oov_tokenがインデックス1を使ってしまうので、
oov_tokenに何か値を指定すると、学習してくれる単語が一つ減ります。

num_words を先ほどと同じように7にしておくと、 the, of, to, and, a, の5単語しか変換しないことがわかります。


keras_tokenizer_oov = Tokenizer(
                            num_words=7,
                            oov_token="未知語"
                        )

# 文字列から学習する
keras_tokenizer_oov.fit_on_texts(text_data)

# 学習した単語とそのindex
print(keras_tokenizer_oov.word_index)
# {'未知語': 1, 'the': 2, 'of': 3, 'to': 4, 'and': 5, 'a': 6, 'in': 7, 'is': 8, 'i': 9, 'that': 10, 'it': 11,...

print(keras_tokenizer_oov.texts_to_sequences(["the of to and a in is i that it for"]))
# [[2, 3, 4, 5, 6, 1, 1, 1, 1, 1, 1]]

oov_token を使うと、元の単語列と、変換後の数列の要素数が維持されますし、
未学習だった単語がどれだけ含まれているのかもわかるのでとても便利ですが、
使える単語が一個減ることなどは注意しないといけませんね。

ちなみに、 oov_token に、 学習対象のドキュメントにある単語を使うと、その単語のインデックスがoov_tokenとして使われ、
1が使われなくなります。
ちょっとややこしいのでできるだけ避けましょう。

KerasのTokenizerの基本的な使い方

自然言語処理において翻訳などのseq2seqモデルやそれ以外でもRNN系のモデルを使う場合、
前処理においてテキストの列を数列に変換(トークン化)することがあります。

そのよな時に、Kerasのユーティリティーに用意されている、Tokenizerが便利なのでその基本的な使い方を紹介します。
今回の主な内容は次の4つです。(その他細かいオプションとか、別の使い側は次回以降の更新で。)
– インスタンスの生成
– テキストを数列化する
– デフォルトパラメーターで生成した時の設定
– 数列をテキストに戻す

ドキュメントはこちらです。

サンプルに何かデータが必要なので、20newsのデータを一部だけ読み込んで使います。


from sklearn.datasets import fetch_20newsgroups

# データの読み込み。少量で良いのでカテゴリも一つに絞る。
remove = ('headers', 'footers', 'quotes')
categorys = [
        "sci.med",
    ]
twenty_news = fetch_20newsgroups(
                                subset='train',
                                remove=remove,
                                categories=categorys
                            )
text_data = twenty_news.data

Tokenizer を使うときはまずはインスタンスを生成し、
テキストデータを学習させる必要があります。
(ここで学習しなかった単語はトークン化できません。)


from tensorflow.keras.preprocessing.text import Tokenizer
# Tokenizer のインスタンス生成
keras_tokenizer = Tokenizer()
# 文字列から学習する
keras_tokenizer.fit_on_texts(text_data)

# 学習した単語とそのindex
print(keras_tokenizer.word_index)
"""
{'the': 1, 'of': 2, 'to': 3, 'and': 4, 'a': 5, 'in': 6, 'is': 7,
 'i': 8, 'that': 9, 'it': 10, 'for': 11, 'this': 12, 'are': 13, ...,
--- 以下略 ---
"""

テキストデータをトークン化するときは、texts_to_sequences に、”テキストデータの配列を”渡します。
テキストを一つだけ渡すと、それを文字単位に分解してしまうので注意してください。


# テキストデータを数列に変更
sequence_data = keras_tokenizer.texts_to_sequences(text_data)
# 一つ目のテキストの変換結果。
print(sequence_data[0])
"""
[780, 3, 1800, 4784, 4785, 3063, 1800, 2596, 10, 41, 130, 24,
15, 4, 148, 388, 2597, 11, 60, 110, 20, 38, 515, 108, 586, 704,
353, 21, 46, 31, 7, 467, 3, 268, 6, 5, 4786, 965, 2223, 43, 2598,
2, 1, 515, 24, 15, 13, 747, 11, 5, 705, 662, 586, 37, 423, 587, 7092,
77, 13, 1490, 3, 130, 16, 5, 2224, 2, 12, 415, 3064, 12, 7, 5, 516,
6, 40, 79, 47, 18, 610, 3732, 1801, 26, 2225, 706, 918, 3065, 2,
1, 1801, 21, 32, 61, 1638, 31, 329, 7, 9, 1, 1802, 966, 1491,
18, 3, 126, 3066, 4, 50, 1352, 3067]
"""

これで目的のトークン化ができました。
今回は、インスタンス化する時に何も引数を渡さず、完全にデフォルトの設定になっているのですが、
一応主な設定を確認しておきましょう。


# デフォルトでは、文字を小文字に揃える。
print(keras_tokenizer.lower)
# True

# デフォルトでは文字単位ではなく、次のsplitで区切った単語単位でトークン化する。
print(keras_tokenizer.char_level)
# False

# デフォルトでは、split に半角スペースが指定されており、スーペースで区切られる。
print(keras_tokenizer.split == " ")

# いくつかの記号は除外され、単語中に含まれている場合はそこで区切られる。
print(keras_tokenizer.filters)
# !"#$%&()*+,-./:;<=>?@[\]^_`{|}~

# 例えば、 dog&cat は &が取り除かれ、 dog と cat が個別にトークン化される。
print(keras_tokenizer.texts_to_sequences(["dog&cat"]))
# [[7316, 2043]]

# & が 半角ペースだった場合と結果は同じ
print(keras_tokenizer.texts_to_sequences(["dog cat"]))
# [[7316, 2043]]

最後に、トークン列をテキストに戻す方法です。
sequences_to_texts を使います。


# 数列をテキストに戻す。
text_data_2 = keras_tokenizer.sequences_to_texts(sequence_data)

print("元のテキスト")
print(text_data[0])
print("\n復元したテキスト")
print(text_data_2[0])

"""
元のテキスト
[reply to keith@actrix.gen.nz (Keith Stewart)]


It would help if you (and anyone else asking for medical information on
some subject) could ask specific questions, as no one is likely to type
in a textbook chapter covering all aspects of the subject.  If you are
looking for a comprehensive review, ask your local hospital librarian.
Most are happy to help with a request of this sort.

Briefly, this is a condition in which patients who have significant
residual weakness from childhood polio notice progression of the
weakness as they get older.  One theory is that the remaining motor
neurons have to work harder and so die sooner.

復元したテキスト
reply to keith actrix gen nz keith stewart it would help if you and anyone
else asking for medical information on some subject could ask specific
questions as no one is likely to type in a textbook chapter covering all
aspects of the subject if you are looking for a comprehensive review ask
your local hospital librarian most are happy to help with a request of this
sort briefly this is a condition in which patients who have significant
residual weakness from childhood polio notice progression of
the weakness as they get older one theory is that the remaining
motor neurons have to work harder and so die sooner
"""

改行のほか、括弧やカンマなどの記号が消えていること、一部の大文字が小文字になっていることなどが確認できます。