gensimでword2vec

とっくに書いたと勘違いしていたのですが、まだ記事にしていなかったことに気づいたので、今更ですがgensimを使って単語の埋め込みを得る方法を紹介します。

word2vec自体の説明はそのうち書きたいですが一旦こちらをご参照ください。
wikipedia: Word2vec

gensim自体はもともとトピックモデル用のライブラリだったようで、
公式サイトのタイトルがズバリ「gensim: Topic modelling for humans」となっています。
ただ自分はもっぱらword2vec(skip-gram/CBOW)の為に使っています。

せっかくなので、このあいだのlivedoorニュースコーパスでやってみましょう。

テキストデータを単語単位で分かち書きした物を「配列で」準備し、
渡してあげればそれだけで学習してくれます。
他のライブラリはスペース区切りの文字列などを受け取ることが多いので、配列で準備する点だけは注意が必要ですね。

今回はgensimの使い方がメインなので、最低限の前処理だけして学習用データを準備します。


import MeCab
import pandas as pd

tagger = MeCab.Tagger("-d /usr/local/lib/mecab/dic/mecab-ipadic-neologd")


def mecab_tokenizer(text):
    # テキストを分かち書きする関数を準備する
    parsed_lines = tagger.parse(text).split("\n")[:-2]
    surfaces = [l.split('\t')[0] for l in parsed_lines]
    features = [l.split('\t')[1] for l in parsed_lines]
    # 原型を取得
    bases = [f.split(',')[6] for f in features]
    # 配列で結果を返す
    token_list = [b if b != '*' else s for s, b in zip(surfaces, bases)]
    # アルファベットを小文字に統一
    token_list = [t.lower() for t in token_list]
    return token_list


# コーパスの見込み (df["text"]にニュース記事本文が入る。)
df = pd.read_csv("./livedoor_news_corpus.csv")


# 不要な文字を消す
stop_chars = "\n,.、。()()「」 『 』[]【】“”!! ??—:・■●★▲▼"
for stop_char in stop_chars:
    df["text"] = df["text"].str.replace(stop_char, " ")

# ユニコード正規化
df["text"] = df["text"].str.normalize("NFKC")
# アルファベットを小文字に統一
df["text"] = df["text"].str.lower()

# 分かち書きしたデータを作成
sentences = df["text"].apply(mecab_tokenizer)

# 作成されたデータのサンプル
print(sentences[:5])
"""
0    [2005年, 11月, から, 翌, 2006年, 7月, まで, 読売新聞, にて, 連...
1    [アンテナ, を, 張る, ながら, 生活, を, する, て, いく, ば, いい, 2月...
2    [3月2日, より, 全国ロードショー, と, なる, スティーブン, スピルバーグ, の,...
3    [女優, の, 香里奈, が, 18日, 都内, で, 行う, れる, た, 映画, ガール...
4    [5日, 東京, 千代田区, の, 内幸町, ホール, にて, 映画, キャプテン, アメリ...
Name: text, dtype: object
"""

さて、このsentencesを学習データとしてモデルを訓練します。
アルゴリズムは skip-gramとCBOWがありますが、今回はski-gramで試します。
使い方は簡単で、モデルをインポートして、インスタンス作成するときにデータを渡すだけです。
skip-gramを使いたい場合はsg=1を指定します。(0はCBOW)


from gensim.models import Word2Vec

word2vec_model = Word2Vec(
        sentences,
        sg=1,
    )

人によっては、次のようにインポート方法が違いますが、結果は同じです。


from gensim.models import word2vec

word2vec_model = word2vec.Word2Vec(
        sentences,
        sg=1,
    )

モデルの種類を指定する sg 以外にも実際には多くの引数をとるので、主なもの(自分がよく設定するもの)紹介しておきます。
=の右に書いているのは初期値です。

– size=100, # 埋め込むベクトルの次元
– window=5, # 前後何単語を予測するかの幅
– min_count=5, # 出現頻度の低い単語の足切り基準
– max_vocab_size=None, # 最大語彙数
– workers=3, # 学習の多重度
– sg=0, # skip-gram: 1 , CBOW: 0
– hs=0,
– negative=5, # negative sampling における負例の個数
– iter=5, # 学習回数

学習済みのモデルは次のように保存できます。ついでに、読み込みにコードも紹介。


# モデルの保存
word2vec_model.save("word2vec.model")

# 読み込み
# word2vec_model = Word2Vec.load("word2vec.model")

さて、モデルができたところで、使っていきましょう。
詳細全然説明してませんが、 king – man + woman = queen などの演算ができるということで、
一時非常に有名になったので、以下の例でも雰囲気伝わるのではないかなと思います。
それぞれの詳細な挙動についてはまた改めて説明記事書きたいです。


# 単語ベクトルを得る。 次の二つの書き方は結果は同じ
word2vec_model.wv["パソコン"]
word2vec_model.wv.get_vector("パソコン")

# 類似度の高い単語を得る。 topn引数で個数を指定(デフォルト10)
word2vec_model.wv.most_similar("パソコン", topn=5)
"""
[('pc', 0.7659528851509094),
 ('ノート', 0.7527473568916321),
 ('windows', 0.7253533601760864),
 ('companion', 0.7214531302452087),
 ('macos x', 0.7181501388549805)]
"""

# 単語の足し算、引き算は positive, negative で引数を指定する
# 下の例は 俳優 - 男 + 女 = 女優
word2vec_model.wv.most_similar(positive=["俳優", "女"], negative=["男"], topn=1)
# [('女優', 0.7674037218093872)]

# 二つの単語の類似度を得る
print(word2vec_model.wv.similarity("巨人", "阪神"))
# 0.8579513

# 仲間はずれ探し。
print(word2vec_model.wv.doesnt_match(["ロッテ", "オリックス", "ヤクルト", "ソニー"]))
# ソニー

# 語彙の一覧を取得する
word2vec_model.wv.vocab.keys()

# 埋め込みベクトルを全て得る。 (サイズは 語彙数*埋め込み次元)
word2vec_model.wv.vectors

livedoorニュースコーパスのファイルをデータフレームにまとめる

前回の記事でダウンロードしてきたlivedoorニュースコーパスのデータを扱いやすいようにデータフレームまとめてしまいます。

ファイルの中には、URL、日時、記事タイトルがあって、そのあとに記事本文が続く構成になっていますが、
それぞれ属性が違うので別列に取り出しています、

このブログでは再帰的なファイルの探索はglobを使うことが多かったのですが、
パスからファイル名やディレクトリ名を取り出して使いたかったので、pathlibの方を使いました。

では早速ですがコードの紹介です。


import pandas as pd
import pathlib

df = pd.DataFrame(columns=["category", "url", "time", "title", "text"])

for file_path in pathlib.Path("./text").glob("**/*.txt"):
    f_path = pathlib.Path(file_path)
    file_name = f_path.name
    category_name = f_path.parent.name

    # 特殊ファイルはスキップ
    if file_name in ["CHANGES.txt", "README.txt", "LICENSE.txt"]:
        continue

    with open(file_path, "r") as f:
        text_all = f.read()
        text_lines = text_all.split("\n")
        url, time, title, *article = text_lines
        article = "\n".join(article)

        df.loc[file_name] = [category_name, url, time, title, article]

# インデックスに使用していたファイル名を列の1つにする。
df.reset_index(inplace=True)
df.rename(columns={"index": "filename"}, inplace=True)

# ファイルに保存
df.to_csv("./livedoor_news_corpus.csv", encoding="utf-8_sig", index=None)

思っていたより短く簡単なコードであっさりできてしまったので前回の記事に含めておけばよかったですね。

livedoorニュースコーパスをダウンロードしてみる

職場ではテキストデータに不自由することはほぼないのですが、自学では自然言語処理のモデルを試す時は
大抵、20newsgroupsを使ってました。
参考: 20ニュースグループのテキストデータを読み込んでみる

ただ、やっぱり自宅での検証でも日本語データを使いことがあるので、
以前から存在だけは知っていたlivedoorニュースコーパスを試してみることにしました。
(wikipediaほど巨大なデータではなく、メロスほど少なくなく、ちょうどいいコーパスが欲しいことがよくあるのです)

これは株式会社ロンウイットさんが、収集して配布してくださっているデータです。
登録も何も必要なく、そのままダウンロードできるので非常に便利です。

ライブドアニュースの以下の9カテゴリのニュース記事が格納されています。
(ただし、時期は結構古いです。)

– トピックニュース
– Sports Watch
– ITライフハック
– 家電チャンネル
– MOVIE ENTER
– 独女通信
– エスマックス
– livedoor HOMME
– Peachy

配布ページはこちらです。
ここから、 ldcc-20140209.tar.gz というファイルをダウンロードします。
gzファイルで配布されているので、 tarコマンドで解凍しましょう。
(僕の環境はMacです)


# 展開
$ tar zfx ldcc-20140209.tar.gz
# 確認
$ ls text
CHANGES.txt    dokujo-tsushin kaden-channel  movie-enter    smax           topic-news
README.txt     it-life-hack   livedoor-homme peachy         sports-watch

展開すると text というディレクトリができ、中にさらに9個のディレクトリが含まれています。
それぞれのディレクトリの中に、
sports-watch-5069031.txt などの名前でテキストファイルが格納されています。
全部で 7378 ファイルあるようですが、 そのうち 9個 はライセンスファイル(LICENSE.txt)で、CHANGES.txt と README.txt を含むので、
データとしては 7378 – 11 = 7367 ファイルがデータとして使えます。


$ find . | grep txt | wc -l
    7378

LICENSEファイルは重要なので使う前に一通り読んでおきましょう。
各記事ファイルにはクリエイティブ・コモンズライセンス「表示 – 改変禁止」
https://creativecommons.org/licenses/by-nd/2.1/jp/)が適用されます。

記事ファイルの中身は次のフォーマットで作成されています。(README.txtの引用)

1行目:記事のURL
2行目:記事の日付
3行目:記事のタイトル
4行目以降:記事の本文

ファイルがバラバラなので、便利に使うには一回集約した方が良さそうですね。
少し考えてみて次の記事あたりで紹介したいと思います。

matplotlibの3次元プロットを回転するアニメーションで保存する

matplotlobで3次元のグラフを作る時、jupyter notebookではグリグリと動かしていろんな角度から確認することができます。
それをそのままこのブログに埋め込みたくて方法を探していたのですが良いのが見つからなかったので代用としてgifアニメーションを作ることにしました。
今回の記事では、Z軸を中心にぐるっと一周回転させてみます。

以前、 matplotlib.animation.ArtistAnimation を使ったgifの作り方は紹介したことがあるので、
今回は matplotlib.animation.FuncAnimation
を使う別の方法を紹介します。

参考記事: matplotlibでgif動画生成

(ちなみにFuncAnimation自体は、かなり柔軟に動画を作ることができ、
当然3Dプロットを回す以外の使い方もできます。)

可視化の対象は前回の記事のサッカーボールです。
変数Gには、前回の記事と同じグラフが格納されているものとしてください。
無駄に長いコードなので重複部分は今回のコードに入れていません。

さて、 FuncAnimation の使い方の紹介です。

この関数は、
fig, func, frames の3つの引数を渡して使います。
figはグラフを描写するfigureオブジェクトです。
framesにはリスト等を渡します。整数値を渡すとrange()と同じ動きになり、0からその整数値-1までの値を渡したのと同じになります。
この、framesに渡したリストの値を順番にfuncに渡して関数が実行され、それぞれの実行結果をつなげたものがアニメーションになります。

今回は少し工夫して、init_func という引数も使います。
これは、最初に一回だけ実行する関数を渡します。

1. init_func で 3次元にグラフをplotする
2. func で少しづつ回転する

という流れで、func では回転以外の操作をしないようにして少しだけ効率的にしました。


import numpy as np
import networkx as nx
import matplotlib.pyplot as plt
from matplotlib.animation import FuncAnimation
from mpl_toolkits.mplot3d import Axes3D

# G に サッカーボルー型のグラフデータを格納する処理は略

# ノードの座標を固定
pos = nx.spring_layout(G, dim=3)
# 辞書型から配列型に変換
pos_ary = np.array([pos[n] for n in G])

# plot する figureと、 Axesを準備する
fig = plt.figure(figsize=(10, 10), facecolor="w")
ax = fig.add_subplot(111, projection="3d")


# Axes にGraph をプロットする関数を準備
def plot_graph():
    ax.scatter(
        pos_ary[:, 0],
        pos_ary[:, 1],
        pos_ary[:, 2],
        s=200,
    )

    # ノードにラベルを表示する
    for n in G.nodes:
        ax.text(*pos[n], n)

    # エッジの表示
    for e in G.edges:
        node0_pos = pos[e[0]]
        node1_pos = pos[e[1]]
        xx = [node0_pos[0], node1_pos[0]]
        yy = [node0_pos[1], node1_pos[1]]
        zz = [node0_pos[2], node1_pos[2]]
        ax.plot(xx, yy, zz, c="#aaaaaa")


# 引数を受け取って図を回転させる関数を準備
def plt_graph3d(angle):
    ax.view_init(azim=angle*5)


# アニメーションを作成
ani = FuncAnimation(
    fig,
    func=plt_graph3d,
    frames=72,
    init_func=plot_graph,
    interval=300
)

# imagemagickで作成したアニメーションをGIFで書き出す
ani.save("rolling.gif", writer="pillow")

出力結果がこちらのgifです。

もともと対称性の高い図形なので、回転させるありがたみが薄かったかもしれないですね。

図形を回転させるところでは、
view_init
という関数を使いました。
elev と azim という二つの引数をとりますが、回転の向きが違います。
使うのは二つ目の azim の方なので注意が必要です。

NetworkXで作成したグラフを3次元にプロットする

NetworkXでグラフを可視化する時、
2次元だとエッジが多すぎていわゆる毛玉状態になり、わけがわからないけど3次元だと少しマシになるということがあったので、3次元でプロットする方法を紹介しておきます。

公式ドキュメントの 3D Drawing のページを見ると、
Mayavi2 というのを使う方法が紹介されています。
ただ、僕がこれを使ったことがないのと、Matplotlibで十分できそうだったので、Matplotlibでやってみました。
Mayavi2 はこれはこれで便利そうですし、可視化の幅を広げられそうなので近いうちに試します。

まず、可視化するグラフデータを生成します。
今回はいつもみたいにランダム生成ではなく、エッジを具体的に指定して構築しました。
出来上がるのはサッカーボール型の多面体です。
(実はこのデータ生成の方が3次元プロットより苦労しました。)


import matplotlib.pyplot as plt
from mpl_toolkits.mplot3d import Axes3D
import networkx as nx
import numpy as np

# エッジデータを生成
edge_list = [
    (0, 1), (1, 2), (2, 3), (3, 4), (4, 0),
    (0, 5), (1, 6), (2, 7), (3, 8), (4, 9),
    (5, 10), (10, 11), (11, 6), (6, 12), (12, 13),
    (13, 7), (7, 14), (14, 15), (15, 8), (8, 16),
    (16, 17), (17, 9), (9, 18), (18, 19), (19, 5),
    (11, 20), (20, 21), (21, 12), (13, 22), (22, 23),
    (23, 14), (15, 24), (24, 25), (25, 16), (17, 26),
    (26, 27), (27, 18), (19, 28), (28, 29), (29, 10),
    (21, 30), (30, 31), (31, 22), (23, 32), (32, 33),
    (33, 24), (25, 34), (34, 35), (35, 26), (27, 36),
    (36, 37), (37, 28), (29, 38), (38, 39), (39, 20),
    (31, 40), (40, 41), (41, 32), (33, 42), (42, 43),
    (43, 34), (35, 44), (44, 45), (45, 36), (37, 46),
    (46, 47), (47, 38), (39, 48), (48, 49), (49, 30),
    (41, 50), (50, 42), (43, 51), (51, 44), (45, 52),
    (52, 46), (47, 53), (53, 48), (49, 54), (54, 40),
    (50, 55), (51, 56), (52, 57), (53, 58), (54, 59),
    (55, 56), (56, 57), (57, 58), (58, 59), (59, 55),
]

# 生成したエッジデータからグラフ作成
G = nx.Graph()
G.add_edges_from(edge_list)

さて、データができたので早速3次元空間にプロットしてみましょう。
方法は簡単で、以前紹介したmatplotlibの3次元プロットの方法で、
ノードとエッジを順番に出力するだけです。

ノードの方はこちらの記事が参考になります。
参考: matplotlibで3D散布図
エッジの方はまだ直接的に消化はしていませんが、2次元空間に直線を引く時と同様に、
ax.plot で描けます。

実際にやってみたのが以下のコードです。
比較用に2次元にプロットしたものを横に並べました。


# spring_layout アルゴリズムで、3次元の座標を生成する
pos = nx.spring_layout(G, dim=3)
# 辞書型から配列型に変換
pos_ary = np.array([pos[n] for n in G])

# ここから可視化
fig = plt.figure(figsize=(20, 10), facecolor="w")
ax = fig.add_subplot(121, projection="3d")

# 各ノードの位置に点を打つ
ax.scatter(
    pos_ary[:, 0],
    pos_ary[:, 1],
    pos_ary[:, 2],
    s=200,
)

# ノードにラベルを表示する
for n in G.nodes:
    ax.text(*pos[n], n)

# エッジの表示
for e in G.edges:
    node0_pos = pos[e[0]]
    node1_pos = pos[e[1]]
    xx = [node0_pos[0], node1_pos[0]]
    yy = [node0_pos[1], node1_pos[1]]
    zz = [node0_pos[2], node1_pos[2]]
    ax.plot(xx, yy, zz, c="#aaaaaa")
    
# 比較用 : 通常の2次元軸へのプロット
ax = fig.add_subplot(122)
nx.draw_networkx(G, edge_color="#aaaaaa")

# 出来上がった図を表示
plt.show()

このコードで以下の図が出力されました。

3次元の方がサッカーボール として綺麗な形になっているのがみて取れると思います。
座標軸の数値はいらないのでこれを消すなどの工夫を加えたらもっと良いかもしれませんね。

blog記事には静止画で貼り付けましたが、
jupyter notebook で実行する時は、
%matplotlib notebook を実行しておくと、
3次元プロットはグリグリ動かして確認ができます。
結構便利なので機会があれば試してみてください。

追記(2022/11/16) : エッジの描写についての解説

3次元にプロットするところの、エッジ(線)の描写部分について質問いただきましたので、解説します。

該当コードはここですね。

for e in G.edges:
    print(e)
    node0_pos = pos[e[0]]
    node1_pos = pos[e[1]]
    xx = [node0_pos[0], node1_pos[0]]
    yy = [node0_pos[1], node1_pos[1]]
    zz = [node0_pos[2], node1_pos[2]]
    ax.plot(xx, yy, zz, c="#aaaaaa")

まず、最初のfor文ですが、これはグラフのエッジをループさせています。変数eエッジの中の一つが格納され、それがどのノードからどのノードへのエッジなのかの情報がただのタプルとして入ってます。1個目だけprintしてみます。一番最初に作ったエッジデータの1個目ですね。

for e in G.edges:
    print(type(e))
    print(e)
    break #  打ち切り
"""
<class 'tuple'>
(0, 1)
"""

エッジが(0, 1)ですから、まずノード0からノード1へ線をひこう、というのが以降の処理です。そのために、ノード0とノード1はどの座標に配置されているのかの情報が必要になります。

その座標が spring_layout ってアルゴリズムで推定して、ノード:座標の形でposって変数に辞書で入ってます。(上の方のコード参照)
中身を見ておきましょう。全ノード分含まれているのですが、最初の5件blogに載せます。

from pprint import pprint


pprint(pos)
"""
{0: array([-0.6114604 ,  0.62195763,  0.31867187]),
 1: array([-0.5150625 ,  0.74024425, -0.01842863]),
 2: array([-0.17471886,  0.96503904,  0.00644528]),
 3: array([-0.02757867,  0.98970947,  0.35055788]),
 4: array([-0.30497656,  0.77198962,  0.53307487]),
# 以下略
""" 

e = (0, 1) ですから、 e[0] = 0, e[1] = 1です。(最初のedgeはインデックスと中身が一致しててややこしく、すみません)
これを使って、エッジが繋いでる2頂点の座標を取得します。アルゴリズムが乱数使っているので具体的な値は実行するたびに変わりますのでご注意ください。

node0_pos = pos[e[0]]
node1_pos = pos[e[1]]

# 一つ目のノードの座標
print(node0_pos)
# [-0.6114604   0.62195763  0.31867187]

# 二つ目のノードの座標
print(node1_pos)
# [-0.5150625   0.74024425 -0.01842863]

具体的な座標が定まったので、この2点の間に線を引きます。これは、Axes3Dをimportした状態のmatplotlibのplotメソッドで実行します。

この時にax.plot(1点目の座標, 2点目の座標)と渡すと動かないのです。
2次元のplotにおいても ax.plot(x座標の一覧, y座標の一覧) とデータを渡すように、3次元plotでもax.plot(x座標の一覧, y座標の一覧, z座標の一覧)とデータを渡す必要があります。

xxとかyyとか変な変数名で恐縮ですが、それを続くコードでやってます。

# x座標, y座標, z座標をそれぞれ取り出し
xx = [node0_pos[0], node1_pos[0]]
yy = [node0_pos[1], node1_pos[1]]
zz = [node0_pos[2], node1_pos[2]]

# 中身確認
print(xx)
# [-0.6114604015979618, -0.5150625045997115]
print(yy)
# [0.6219576319265612, 0.740244253223188]
print(zz)
# [0.3186718713992598, -0.01842863446943393]

そして、出来上がったxx,yy,zz を ax.plot()に渡してエッジが1本引けたことになります。
c=”#aaaaaa” はただの色設定(灰色)なので問題ないと思います。

これで1つ引けるので、あとはfor文で各エッジを変数eに格納して順次繰り返しています。

globでサブフォルダを含めて再帰的にファイルを探索する

普段は、DBに格納された扱いやすいデータや1ファイルにまとめられたデータばかり扱っていて、
散らばったファイルからデータを拾ってくることは少ない恵まれた環境で仕事しています。
しかし、久々にあるフォルダ配下に散ってるファイルを再帰的に探してまとめて処理する機会があったのでそのメモです。

以前、特定のフォルダの直下のファイルは、 globで手軽に見つけられるという記事を書きました。
参考: globで手軽にファイル名の一覧を取得する

今回は pathlib を紹介しようと思っていたのですが、
よく globのドキュメントを見ると、 バージョン 3.5 から、再帰的なglobが実装されていたんですね。
参考: glob — Unix 形式のパス名のパターン展開

ということでこちらを使ってみます。
recursive に True を指定し、 pathname の中に ** を含めればいいようです。

拡張子付きのファイルパスだけリストアップするには次のように書きます。


import glob
for f in glob.glob("./**/*.*", recursive=True):
    print(f)

"""
./001.txt
./folder01/002.txt
./folder01/003.sql
./folder01/subfolder011/004.txt
./folder01/subfolder011/005.sql
./folder02/006.png
./folder02/subfolder021/007.gif
"""

recursive=False (デフォルト) の場合と一応比較しておきましょう。


for f in glob.glob("./**/*.*", recursive=False):
    print(f)

"""
./folder01/002.txt
./folder01/003.sql
./folder02/006.png
"""

for f in glob.glob("./*/*.*", recursive=False):
    print(f)

"""
./folder01/002.txt
./folder01/003.sql
./folder02/006.png
"""

比較用に ** を * に変えたものも一緒に載せましたが、
recursive=False の場合は、 ** は * と同じ挙動しかしていないことがわかります。

recursive=True にすると、 ** は複数階層のフォルダ(ディレクトリ)も含めて探索してくれています。

特定拡張子のファイルのみ欲しい時は、 pathname の記述で指定しましょう。
ディレクトリだけ指定したい時は / で終えれば可能です。
また、 glob.glob の代わりに、 glob.iglob を使うと、結果をリストではなくイテレーターで返してくれます。


for f in glob.iglob("./**/*.txt", recursive=True):
    print(f)

"""
./001.txt
./folder01/002.txt
./folder01/subfolder011/004.txt
"""


for f in glob.iglob("./**/", recursive=True):
    print(f)

"""
./
./folder01/
./folder01/subfolder012/
./folder01/subfolder011/
./folder02/
./folder02/subfolder021/
"""

望む結果が得られました。

NumPyで行列の固有値と固有ベクトルを求める

最近のNetworkx関係の記事でよく行列の固有ベクトルを求めていますが、
そこで使っているNumPyの関数について紹介します。

最初に行列の固有値と固有ベクトルの定義について復習しておきます。
$\mathbf{A}$を正方行列とします。
この時、スカラー$\lambda$と、零でないベクトル$\mathbf{x}$が、
$$
\mathbf{A}\mathbf{x} = \lambda \mathbf{x}
$$
という関係を満たす時、
$\mathbf{x}$を$\mathbf{A}$の固有ベクトル、$\lambda$を$\mathbf{A}$の固有値と呼びます。

最近のネットワーク分析系の記事でも頻出しているだけでなく、
数学やデータ分析の各所に登場する非常に重要な概念です。

NumPyでは、
numpy.linalg.eig と、 numpy.linalg.eigh として実装されています。

早速、適当な行列に対して使ってみます。


import numpy as np
a = np.array(
        [[-2, -1,  2],
         [1,  4,  3],
         [1,  1,  2]]
    )
print(a)
"""
[[-2 -1  2]
 [ 1  4  3]
 [ 1  1  2]]
 """

# 固有値のリストと、固有ベクトルを列に持つ行列のタプルが戻る
values, vectors = np.linalg.eig(a)
print(values)
# [-2.37646808  4.92356209  1.452906  ]

print(vectors)
"""
[[ 0.97606147  0.04809876  0.4845743 ]
 [-0.05394264 -0.95000852 -0.73987868]
 [-0.21069932 -0.30849687  0.46665542]]
"""

eig一発で、固有値と固有ベクトルをまとめて返してくれるのでとても手軽ですね。
上のサンプルコードのように、それぞれ別の変数で受け取るのがオススメです。

なお、一つの変数で受け取ることもできます。
結果を見ていただければ若干使いにくそうな雰囲気が伝わると思います。


eig_result =  np.linalg.eig(a)
print(eig_result)
"""
(array([-2.37646808,  4.92356209,  1.452906  ]), array([[ 0.97606147,  0.04809876,  0.4845743 ],
       [-0.05394264, -0.95000852, -0.73987868],
       [-0.21069932, -0.30849687,  0.46665542]]))
"""

さて、固有値の方はvalues に入っている値がそれぞれ求めたかった値になりますが、
固有ベクトルの方は少し注意が必要です。 というのもサンプルコードの、コメントに書いている通り、
固有ベクトルは、結果の行列の列ベクトルとして格納されています。

つまり、 vectors[0], vectors[1], vectors[2] は固有ベクトルではありません。
正しい固有ベクトルは、 vectors[:, 0], vectors[:, 1], vectors[:, 2] です。
それぞれ、values[0], values[1], values[2] に対応します。
なお、固有ベクトルを0でないスカラー倍したものはそれもまた同じ固有値の固有ベクトルになりますが、
このeigの戻り値は、単位ベクトル(長さが1)になるように正規化されて戻されます。

一応、固有値と固有ベクトルの定義の両辺をそれぞれ計算して、
これらの値が本当に固有値と固有ベクトルなのか見ておきましょう。


for i in range(3):
    print(values[i] * vectors[:, i])
    print(a @ vectors[:, i])


"""
[-2.31957893  0.12819296  0.5007202 ]
[-2.31957893  0.12819296  0.5007202 ]
[ 0.23681725 -4.67742593 -1.5189035 ]
[ 0.23681725 -4.67742593 -1.5189035 ]
[ 0.70404091 -1.07497418  0.67800646]
[ 0.70404091 -1.07497418  0.67800646]
"""

バッチリですね。

もう一つのeighについての紹介です。
eigは一般の正方行列に対して利用できますが、 eighは、実対称行列と、エルミート行列に対してのみ利用できます。
なお、eighは行列の下三角行列部分だけ使って計算するので、
どちらでもない行列を渡しても普通に動いてしまいます。結果は不正確なので注意が必要です。

ついでに紹介しますが、
numpy.linalg.eigvals と、 numpy.linalg.eigvals というメソッドで、
固有値のみを得ることもできます。
固有ベクトルが不要なら、eig の戻り値の該当部分を捨てれば済むのであまり使ったことはないのですが、
メモリの節約や計算速度等のメリットがあるのかもしれません。

WordPressの記事URL変更のためにリダイレクトの設定をする

ずっとこのブログのカテゴリを整理したかったのですが、先延ばしにしているうちに「プログラミング」の記事が150を超え、
このブログの半分程度をしめるようになってしまいました。

明確に困るということはないのですが、もう少しカテゴリーを整理したいと思っています。
その時に問題になるのがパーマリンク(URL)です。

このブログではパーマリングにURLを含めているのでカテゴリーを見直すとURLが変わってしまいます。
そうなると、せっかく検索などから訪問してくれる人がいらしても目的のページにたどり着きませんし、
各記事間のリンクもリンク切れになってしまいます。

そこで、ありきたりな方法ですがリダイレクト設定を入れていくことにしました。
手軽に設定する方法を調べたところ、
Redirection というプラグインが定番のようなのでこれを使います。
参考 : Redirectionプラグインのページ

いつものように、Wordpress管理画面の左ペインのプラグインから、新規追加で検索してインストールと有効化します。
ツールのところに Redirection が現れたので選択。
みたところ、初期設定が必要なようでした。

オプションとして、次の3項目がありました。
Monitor permalink changes in WordPress posts and pages.
Keep a log of all redirects and 404 errors.
Store IP information for redirects and 404 errors.

どうやらパーマリンクの変更を勝手に検知して設定を入れてくれたり、404エラーを監視してくれたりするようです。
実はリンク変更時の設定は全部手作業でやらないといけないといけないと勘違いしていたので非常にありがたいです。
チェック入れて進みます。(後でも変更できるそうです)

最後に、既存の設定をインポートするかどうか聞かれて終了になりました。
(既存の設定に何も心当たりがなかったのですが、以前タイプミスして一瞬だけ間違って公開し、すぐ修正したULRが取り込まれました。)

これであとは、リダイレクト元とリダイレクト先のURLを設定していけば、使えます。

設定画面からログの保存期間等も設定できるので、慣れるまでは長めに保存するよう変えておきました。
(デフォルト 1週間、 設定変更後 1ヶ月)

Pandasで欠損のある列の文字列型の数値を数値型に変換する

イケてるタイトルがつけられなくて申し訳ない。

pandas.to_numeric という関数の errors という引数が便利なことを知ったのでそれを紹介します。

データを扱っている時、文字列型の数字を数値型に型変換したいことはよくあります。

単体の変数であれば、 intflaotで変換できます。


int("123") #123
float("123") # 123.0

DataFrameや Series でも、全ての値が問題なく変換できる場合は、 .astypeで変換できます。


data1_str = pd.Series(["1", "2", "3"])
print(data1_str)
"""
0    1
1    2
2    3
dtype: object
"""

data1_int = data1_str.astype(int)
print(data1_int)
"""
0    1
1    2
2    3
dtype: int64
"""

ここで厄介なのが、元の値の中に、欠損値や数値に変換できない値が混ざっている場合です。
.astype(int).astype(float)すると、エラーが発生します。
astypeメソッド自体も、errorという引数をとりますが、
エラーを ignore で抑制した場合、変換は一切行ってくれません。
参考: pandas.Series.astype

僕が期待しているのは、 数値型に変換できる値は数値型に変換して、Noneや変換できない文字列は NaNで埋めてくれることです。
そして、それが、pandas.to_numericを使うと手軽に実現できます。


import pandas as pd

# ダミーデータ生成
df = pd.DataFrame(
    {
        "key": ["key1", "key2", "key3", "key4"],
        "value_str": ["123", "45.67", None, "八十九"],
    }
)

# value_str 列の値を数値に変えられるものは変えた列を作る
df["value_num"] = pd.to_numeric(df["value_str"], errors="coerce")

print(df)
"""
    key value_str  value_num
0  key1       123     123.00
1  key2     45.67      45.67
2  key3      None        NaN
3  key4       八十九        NaN
"""

バッチリできました。

ポイントは、 errors="coerce"の部分です。
errorsは、”ignore”, “raise”, “coerce” の3種類の値を取ります。
“raise” がデフォルトで、これを指定すると普通に例外が発生します。
“ignore” は例外を抑えますが、何の変換もせず、そのままのオブジェクトを返します。
“coerce” は、数値に変換できるものは変換して、そうでないものはNaNにしてくれます。

“raise”だとこのような例外が発生します。


try:
    pd.to_numeric(df["value_str"], errors="raise")
except Exception as e:
    print(e)

# Unable to parse string "八十九" at position 3

文字列を時刻に変える to_datetime や、汎用的な型変換の astype に比べてマイナーな印象があるのですが、
地味に使える場面が多いので、数値への型変換の機会があったら、
to_numeric を試してみてください。