静かなる名辞

pythonと読書

【python】numpyのmatrix・matの使い方

 numpyにはみんな大好きndarrayと、普段あまり目にしないmatrixがある*1。matrixは行列演算が行いやすいようにデザインされており、それはそれで良いんだけどndarrayとは微妙に使い勝手が異なるせいで慣れるまでは微妙に戸惑う可能性がある。

 たとえば、対話型インタプリタでこんなコードを打ってみる。

>>> import numpy as np
>>> a = [[1,2,3],
...      [4,5,6],
...      [7,8,9]]
>>> a_arr = np.array(a)
>>> a_mat = np.matrix(a)
>>> a_arr[0][0]
1
>>> a_mat[0][0])
matrix([[1, 2, 3]])

 挙動が違う。numpy配列では、[0][0]と指定すると「1行目を切り出したndarrayを生成する」→「その0番目の要素の値を返す」という処理がされる訳だが、matrixだと「1行目の行だけ切り出した行列を作る」→「生成された1行だけの行列の1行目を切り出してまた行列を作る」という処理になってしまう。だから、何回やっても同じものが出てくる。極端な話、こう書いても同じ。

>>> a_mat[0][0][0][0]
matrix([[1, 2, 3]])

 もし(0, 0)の要素が欲しいときは次のように書くのが正しい作法である。ndarrayの場合でもこの書き方が正しくて、C言語の添字風に角括弧を続けて書くやり方だと一回ndarrayが生成される分パフォーマンスで不利になるはず(ついさっき気づいたので、ひょっとしたら過去記事で使っちゃってるのがあるかも)。

>>> a_arr[0, 0]
1
>>> a_mat[0, 0]
1

 今度は、列を切り出してみよう。これは基本に忠実にスライスすればできる。

>>> a_arr[:,0]
array([1, 4, 7])
>>> a_mat[:,0]
matrix([[1],
        [4],
        [7]])

 これも挙動が違うのがお分かりいただけただろうか。ndarrayの方では「1列目を切り出した値を格納した1次元配列」が生成されるが、matrixの方では「3行1列の行列」が出て来る。

 そして、ndarrayとmatrixでは掛け算を行った結果もまったく違ってくる。

>>> a_arr*a_arr
array([[ 1,  4,  9],
       [16, 25, 36],
       [49, 64, 81]])
>>> a_mat*a_mat
matrix([[ 30,  36,  42],
        [ 66,  81,  96],
        [102, 126, 150]])

 ndarrayの方で掛け算をして得られる値はいわゆるアダマール*2で、早い話対応する要素同士の掛け算である。一方、matrixの方は行列積をそのまま計算してくれる。

 ちなみに、matrixでも掛け算以外の演算(足し算、引き算、除算)は素直にndarrayと同様値同士で処理されるっぽい。また、matrixでアダマール積を計算したい場合、np.multiplyを使うと良い。

>>> np.multiply(a_mat, a_mat)
matrix([[ 1,  4,  9],
        [16, 25, 36],
        [49, 64, 81]])

 また、matrixでは転置行列や逆行列なども容易に計算できる。これについてはドキュメントを参照のこと。

numpy.matrix — NumPy v1.13 Manual

まとめ

 ここまでお膳立てされているものを使わないのは勿体無いので、線形代数を用いるアルゴリズムを書くときには活用してあげよう。

*1:なお、np.matrixとnp.matは厳密には別物である。この記事が参考になると思う。 numpyのmatrixとmatは違うものなんですね - Qiita

*2:アダマール積 - Wikipedia

【python】pythonで情報エントロピーの計算

 情報理論でとてもよく出てくる情報エントロピー。計算するにはどうすれば良いのだろう?

 選択肢1:定義どおり作る
 定義はとても簡単です。注意するべきことは、入力に0が来たときのパターンを想定しないで作るとmath domain errorが出る(\log_20は定義できないからね・・・)。

from math import log2

def H(lst):
    return -sum([x*log2(x) if x != 0 else 0 for x in lst])

print(H([0.5, 0.5])) # 1.0


 選択肢2:scipyを使う
 scipyにそのものずばりのentropyという関数がある。

scipy.stats.entropy — SciPy v1.0.0 Reference Guide

 確率として渡してあげなくても(ぜんぶ足して1にならないケース)正規化してくれたり、カルバック・ライブラー情報量が計算できるオプションがあったり色々素敵なのだが、デフォルトでは\logの底がeだったりする。ちゃんとbaseを指定してあげよう。

from scipy.stats import entropy

print(entropy([0.5, 0.5], base=2)) # 1.0

【python】numpyで最小二乗法を実装(線形、多項式、正則化など)

 最小二乗法をnumpyで実装してみた。
 理論背景についてはこちらを参照。
mathtrain.jp

www.slideshare.net
qiita.com

やるべきこと

 最小二乗法(正確には線形基底関数モデルによる回帰)は目的変数を説明変数の線形結合で表現しようというアイデア。面倒くさいことをすっ飛ばして言うと、次の式を解いて重みベクトル\vec{w}を求めれば良い。この\vec{w}は誤差関数(誤差のニ乗和)を最小化するようなパラメータになっている。

{ \displaystyle
  A = \left(
    \begin{array}{ccccc}
	1		&	x_{11}	&	x_{12}	&	\ldots 	&	x_{1n} \\
	1		&	x_{21}	&	x_{22}	&	\ldots 	&	x_{2n} \\
	\vdots	&	\vdots	&	\vdots	&	\ddots	&	\vdots \\
	1		&	x_{m1}	&	x_{m2}	&	\ldots	&	x_{mn}
    \end{array}
  \right)
}

{ \displaystyle
\vec{w}=(A^TA)^{-1}A^T\vec{y}
}
 An次元の横ベクトルをm個縦に並べたもの・・・で、よくsklearnに入力しているnumpy配列と変わらない。1列目がぜんぶ1になっているのはバイアス項といって、要するに目的変数が(x-yグラフのy軸と考えて)上にずれたり下にずれたりするのを表現するもの。
 この重みベクトル\vec{w}をどう使うか? お察しの通り、未知の入力を(バイアス項を足して)この重みと掛け算してぜんぶ加算してあげると、推定された目的変数が出てきます・・・。
 でもこれだと線形関数しか回帰できないので、多項式を使って非線形関数にも対応させてあげる。別に難しいことはなく、入力ベクトルの次元を増やし、そこにそういった関数に入力を入れた値を突っ込んでやるだけ。

{ \displaystyle
  A = \left(
    \begin{array}{ccccc}
	1		&	x_{11}	&	x_{12}	&	\ldots 	&	x_{1n} 	&	f_1(x_{11}	)	&	\ldots	&	f_2(x_{11}	)	&\ldots\\
	1		&	x_{21}	&	x_{22}	&	\ldots 	&	x_{2n} 	&	f_1(x_{21}	)	&	\ldots	&	f_2(x_{21}	)	&\ldots\\
	\vdots	&	\vdots	&	\vdots	&	\ddots	&	\vdots 	&	\vdots		&	\ldots	&	\vdots		&\ldots\\
	1		&	x_{m1}	&	x_{m2}	&	\ldots	&	x_{mn}	&	f_1(x_{m1})	&	\ldots	&	f_2(x_{m1})	&\ldots		
    \end{array}
  \right)
}
 簡単そうで素晴らしい。また、正則化を行いたい場合は
{ \displaystyle
\vec{w}=(A^TA+\lambda I)^{-1}A^T\vec{y}
}
 これでいい(L2正則化)。別にL2が正則化のすべてではないけど、一番実装が楽そうなのはこれ。ここでI単位行列(行列の対角線上がぜんぶ1の奴)、\lambda正則化パラメータで数字を大きくすればするほど正則化が強く働く。適正値はパラメタチューニングして探す必要がある。
 L2正則化過学習を防止し、汎化性能を高める効果がある。どうしてL2正則化過学習を防げるのか? 定性的な説明で済ませるが、要するにこれを足すことで\vec{w}の長さ(L2ノルム)が大きくなりすぎないようにできる。最小二乗法で教師データへのフィッティングを極限まで高めようとすると、とにかく複雑な関数をたくさん使って正確にフィットさせよう・・・! となるので、結果的に\vec{w}の長さが大きくなる。こうなることを抑制すると、逆にできるだけ単純な関数の和で行こう・・・ってなる。そんな感じ。

実装

 sklearn風に最小二乗法のクラスを作り、fitとpredictが呼べるようにすると使いやすい。まずLSM(Least Squares Method)クラスを作ろう。
 パラメータは色々あると思うが、とりあえず「基底関数どうするか(なし、多項式)」と「L2正則化するか」と「L2正則化\lambdaをどれくらいにするか」だけ決められるようにしておく。基底関数に関しては、先に関数のリストを作っておいて後から便利に使う方針を取ることにする。 多項式は10次多項式とする。本当はどれくらい次数を上げると良いのかもチューニングするべきだと思う・・・。

import numpy as np
from itertools import product

class LSM:
    def __init__(self, func=None, l2=False, l2_lambda=1):
        self.func = func
        self.func_list = self._gen_func_list(func)
        self.l2 = l2
        self.l2_lambda = l2_lambda
    
    def _gen_func_list(self, func):
        if func is None:
            return [np.vectorize(lambda x:x)]
        elif func == "poly":
            return [self._gen_poly(i) for i in range(1, 11)]
        else:
            raise Exception

    def _gen_poly(self, i):
        return np.vectorize(lambda x:np.power(x, i))

 クロージャ使って綺麗に書こうとしたら微妙に禍々しくなった。でもまあ、こうやって下ごしらえしておけば後は楽である。あとは無心でfitとpredictを作るだけである。コツはndarrayで処理しようとすると色々面倒くさいので、素直にnp.mat型で扱うこと。

class LSM:
    # 中略
    def fit(self, X, y):
        A = np.mat(np.hstack([np.c_[np.ones(X.shape[0])]] + 
                             [f(X) for f in self.func_list]))
        A_t = A.T
        y_mat = np.mat(y).T
        if self.l2:
            lambda_I = self.l2_lambda*np.mat(np.identity(A.shape[1]))
            self.w = ((((A_t*A) + lambda_I ).I)*A_t*y_mat)
        else:
            self.w = (((A_t*A).I)*A_t*y_mat)

    def predict(self, X):
        X = np.mat(np.hstack([np.c_[np.ones(X.shape[0])]] + 
                             [f(X) for f in self.func_list]))
        lst = []
        for x in X:
            lst.append((x*self.w)[0,0])
        return np.array(lst)

 こんな感じ。あとは試しにsin関数(を適当な区間で切り取って更に誤差を足したもの)でも回帰させてみる。汚いけど、テストに使ったプログラムをぜんぶ載せてみる。

# coding: UTF-8
import numpy as np
from sklearn.metrics import mean_squared_error as mse

import matplotlib.pyplot as plt

def rmse(y1, y2):
    return np.sqrt(mse(y1,y2))

def scatter(x_train, y_train, x_test, y_test, y_preds, filename):
    fig = plt.figure()
    
    ax = fig.add_subplot(1,1,1)
    ax.scatter(x_train, y_train, color="b", label="train data")
    ax.plot(x_test, y_test, color="r", label="true line")
    ax.plot(x_test, y_preds, color="g", label="predicted line")
    ax.set_xlabel("rmse:{0:.6f}".format(rmse(y_test, y_preds)))
    ax.set_ylabel("")
    plt.legend()
    plt.savefig(filename)

def gen_sin_data(a=1, b=0, n=20, v=5, x_range=20):
    X = np.array(sorted((np.random.rand(n) - np.array([0.5]*n))*np.array([x_range]*n)))
    Y = np.sin(X)*a + np.array([b]*n) + np.random.normal(0, v, n)
    return X, Y

class LSM:
    def __init__(self, func=None, l2=False, l2_lambda=1):
        self.func = func
        self.func_list = self._gen_func_list(func)
        self.l2 = l2
        self.l2_lambda = l2_lambda
    
    def _gen_func_list(self, func):
        if func is None:
            return [np.vectorize(lambda x:x)]
        elif func == "poly":
            return [self._gen_poly(i) for i in range(1, 11)]
        else:
            raise Exception

    def _gen_poly(self, i):
        return np.vectorize(lambda x:np.power(x, i))

    def fit(self, X, y):
        A = np.mat(np.hstack([np.c_[np.ones(X.shape[0])]] + 
                             [f(X) for f in self.func_list]))
        A_t = A.T
        y_mat = np.mat(y).T
        if self.l2:
            lambda_I = self.l2_lambda*np.mat(np.identity(A.shape[1]))
            self.w = ((((A_t*A) + lambda_I ).I)*A_t*y_mat)
        else:
            self.w = (((A_t*A).I)*A_t*y_mat)

    def predict(self, X):
        X = np.mat(np.hstack([np.c_[np.ones(X.shape[0])]] + 
                             [f(X) for f in self.func_list]))
        lst = []
        for x in X:
            lst.append((x*self.w)[0,0])
        return np.array(lst)

def main():
    X_train, y_train = gen_sin_data(n=50, v=0.4)
    X_test, y_test = gen_sin_data(n=5000, v=0.0)

    X_train_v = np.array(np.mat(X_train).T)
    X_test_v = np.array(np.mat(X_test).T)

    for l in [0, 0.01, 0.1, 1]:
        print("linear reg  lambda:", l)
        lsm = LSM()
        lsm.fit(X_train_v, y_train)
        y_preds = lsm.predict(X_test_v)
        print("rmse:", rmse(y_test, y_preds))
        scatter(X_train, y_train, X_test,  y_test, y_preds, "lin_{0:.2f}.png".format(l))

        print("poly reg  lambda:", l)
        lsm = LSM(func="poly", l2=True, l2_lambda=l)
        lsm.fit(X_train_v, y_train)
        y_preds = lsm.predict(X_test_v)
        print("rmse:", rmse(y_test, y_preds))
        scatter(X_train, y_train, X_test,  y_test, y_preds, "poly_{0:.2f}.png".format(l))
        
if __name__ == '__main__':
    main() 

 実行すると次のようにRMSEが表示される。

linear reg  lambda: 0
rmse: 0.723655771604
poly reg  lambda: 0
rmse: 0.311310262135
linear reg  lambda: 0.01
rmse: 0.723655771604
poly reg  lambda: 0.01
rmse: 0.311303335228
linear reg  lambda: 0.1
rmse: 0.723655771604
poly reg  lambda: 0.1
rmse: 0.311251369924
linear reg  lambda: 1
rmse: 0.723655771604
poly reg  lambda: 1
rmse: 0.311606815936

 線形、多項式でそれぞれ一番いいのを見せてみる。

f:id:hayataka2049:20180121195938p:plain
線形 \lambda=0
f:id:hayataka2049:20180121195947p:plain
多項式 \lambda=0.10

 まあまあ上手く行ってるんだと思う。正則化しなくてもあんまり暴れないみたいだけど、データ数が少なくなると正則化は効いてくると思う。

 ちなみに、sklearnだとlinear_modelあたりを使うと同じことができるはずです。

なぜpython使いは単純なfor文より醜悪なリスト内包表記を好むのか

 煽りっぽいタイトルだが、この記事は真剣である。リスト内包表記にはpython哲学の本質に関わる問題が潜んでいる。
 python使いはリスト内包表記を好む。他の言語の使用者なら「for文で書きゃ良いのに」と思うような処理を、リスト内包表記で書くことを好む。
 それはなぜなのか。

# 1~10の値を2乗して合算し、表示する
# よくあるpythonコード
print( sum([pow(x, 2) for x in range(1, 11)]) )

# こうきゃ良いのにと思う人が世間には多い(と思う)
n = 0
for i in range(1, 11):
    n += pow(i, 2)
print(n)

# 他の言語の使用者が「ぎゅうぎゅうしててわかりづらいよ」と文句をいうと、python使いは不満げに改行して「これでいいだろ、上等だ」という表情を見せる
print( sum([pow(x, 2)
                 for x in range(1, 11)]) )

 まったく理解しがたい習性だと思われるかもしれない。確かにリスト内包表記は多少行数は減らせるけど、そのぶん詰まって読みづらくなっているだけだ、と常識的な感性の持ち主なら誰でも思う。
 python使いがリスト内包表記を使うのは決して可読性のためではない。ぶっちゃけ読みやすさは(読み手の感性に依存するが)大して変わらないからだ。普通の言語に慣れた人は、for文で書いた方が読みやすいと思うだろう。
 パフォーマンスを改善するためでもない。リスト内包で稼げるわずかなパフォーマンス上のメリットなんか米粒のように隠れてしまう複雑な処理を、python使いは平気でリスト内包で処理する。ヘタしたらリスト内包表記にした方が遅くなるくらいなのだが、それでもやるのだ。たとえばpython使いが形態素解析を行いたいと思ったとき、日本語テキストのリストをリスト内包表記でMeCabに投げつける光景がしばしば見られる*1。遅いpythonスクリプトを並列化して処理速度を稼ぐために、リスト内包表記てんこもりの関数をPool.mapで並列実行するなんて本当にありふれた光景だ*2。処理内容によってはメモリが悲鳴をあげ、それ以上領域を確保できなくなってスクリプトが強制終了することすらよくある。python使いは「あちゃー、チューニングするか」とか言って、ソースコードを適当に誤魔化し限られたメモリ上でスクリプトがなんとか動くように書き直してしまう。最初に書いたリスト内包表記は最後まで手もつけられないまま温存されたままだ。
 なんでリスト内包表記なんて使うのか、度し難い愚かさだ。だけど、文句を言い続けるより、もう少し色んな例を考えてみよう。たとえば、かけ算九九を格納したリストを生成して表示するなんてどうだろう。

# リスト内包表記を使わない場合
lst1 = []
for i in range(1,10):
    lst2 = []
    for j in range(1,10):
        lst2.append(i*j)
    lst1.append(lst2)
pprint(lst1)

# 使う場合。可読性を考慮して改行してある
pprint([[i*j for j in range(1,10)]
            for i in range(1,10)])

 これだとリスト内包表記は際立ってシンプルに見える。どうやら、ちょっと利点が見えてきた。何しろ勝手に空リストを作って要素を入れていってくれるんだから楽ちんだ。
 だが、それなら別に大した話じゃない。リスト内包表記は特定の状況下では至って便利なシンタックスシュガーだ、というだけのことである。僕が言いたいのはそんなことではない。
 このかけ算九九の表を生成するプログラムを、ちょっと書き換えよう。表を作っているとき、もし表に追加する値が3の倍数なら、値をprintすることにする。printはリスト内包表記じゃ使えないじゃんと思われる方もいるかもしれないが、なに難しいことはない。値を受け取って3の倍数ならprintする、3の倍数であるかどうかに関わらず受け取った値はそのまま返すという関数を定義することにしよう。

def f(x):
    if x % 3 == 0:
        print(x)
    return x

pprint([[f(i*j) for j in range(1,10)]
            for i in range(1,10)])

 至って簡単。でも、普通わざわざこんなことはしない。for文で愚直に書くとこうなる。

lst1 = []
for i in range(1,10):
    lst2 = []
    for j in range(1,10):
        n = i*j
        if n % 3 == 0:
            print(n)
        lst2.append(n)
    lst1.append(lst2)
pprint(lst1)

 このコードはpython言語の本質的な問題を曝け出している。
 端的に言おう。python言語はオフサイドルールを採用しているので、for文で深いブロックのネストを書いて、インデントが狂うと死ぬ。
 エディタ上でうっかりTabキーを叩くとこうなるという例をいくつか示す。

lst1 = []
for i in range(1,10):
    lst2 = []
    for j in range(1,10):
        n = i*j
            if n % 3 == 0:
            print(n)
        lst2.append(n)
    lst1.append(lst2)
pprint(lst1)
lst1 = []
for i in range(1,10):
    lst2 = []
    for j in range(1,10):
        n = i*j
        if n % 3 == 0:
            print(n)
            lst2.append(n)
    lst1.append(lst2)
pprint(lst1)
lst1 = []
for i in range(1,10):
    lst2 = []
    for j in range(1,10):
        n = i*j
    if n % 3 == 0:
            print(n)
        lst2.append(n)
    lst1.append(lst2)
pprint(lst1)

 うっかりTabキーを叩いてしまった場所によって、コードは動かなくなったり、動かなくならなかったりする。
 動かなくなる場合はまだ良い。エラーを見て修正できる。ただし、ケースによっては「どの行が出っ張っているのか、あるいは引っ込んでいるのか」容易に判断できないかもしれない。簡単に直して終わり、とはいかない。
 動かなくならない場合は、最悪だ。処理のフローが変化したことに気づかないままプログラムを実行して、わかりづらいバグに頭を悩まされるかもしれない。インデントが狂ったことに気づいても、元々の処理を覚えていないと修復できない。この修復にはゼロから同じコードを書くのと同程度の労力が必要になるかもしれない。「自分が何を書いたのか思い出す」ことから始める必要があるのだから。
 リスト内包表記にはこういう問題はない。理由は単純で、ブロックの境界が明確だからだ。エディタのハイライト支援も効くだろうから、カッコのネストを深くしていくのはそう難しくないし、for文で書いた場合と比べてバグの温床になる機会も圧倒的に少ない。うっかりカッコの位置をずらしてしまったり、開きカッコと閉じカッコの対応が付けられなくなっても、シンタックスエラーになるからflymakeを見ながら修正すれば良い。

 おわかり頂けただろうか。
 pythonオフサイドルールは爆弾みたいなもので、そんなものを信頼してコードを書いていたら命が幾つあっても足りないのである。その点リスト内包表記は他の(オフサイドルールではない)言語のブロック構文と同様に信頼できる。だから、ブロックが重要になる処理(つまり大抵の処理)では、python使いは可能ならリスト内包表記で書きたいと思うのである。

 では、pythonオフサイドルールを採用したことは失敗だったのか? これはなんとも言えないだろう。オフサイドルールのデメリットは上記の通りだが、メリットも当然たくさんある。なんといっても見やすく、書きやすく、閉じカッコのために1行を費やしてディスプレイの有効活用できる範囲を狭めるなんて真似も必要ない。また、この記事で述べたように、オフサイドルールの言語ではブロックのネストを深めることは爆弾の山を積み上げる行為に等しい。言語自体にブロックのネストを抑制する機能が備わっていると解釈すれば、pythonを使うことで良質なコードが生まれやすくなる効果があるかもしれない(本当かよ)。
 ただ、もしpythonにリスト内包表記という「インデントブロックの代用手段」がなければ、pythonはまるっきり使い物にならないプログラミング言語になったことは確実と思われる。まあ、それならmap,reduce,filterで代用したかもしれないが・・・

 なぜpython使いはリスト内包表記を好むのか。それは、端的に言えば「for文が使い物にならない」というこの言語の悲しすぎる現実のせいだった。

*1:というか僕がしょっちゅうやってます。反省するべきなような気がしないでもない

*2:僕がしょっちゅう(略

wifiアダプタのないlinux PCを無線LANに繋ぐ

 先日新しくPCを組みましたが、家庭の事情で無線LANしか繋げない状況でした(無線LANルータとか全部1階にあるのに2階にPC置かざるを得ない、みたいな状況だと思いねぇ)。しかし、使うマザーには無線機能などありません。こういうとき、windowsはどうにでもなりますが、linuxマシンだとデバイスドライバなどの絡みで中々難儀な羽目になるケースが多いので、どんな方法があるのか調べてみました。

選択肢一覧

  1. USB接続のwifiアダプタ
  2. PCIwifiカード
  3. 無線LANルータをイーサネットコンバータにする
1.USB接続のwifiアダプタ

 USBポートに挿すだけでwifiが繋がるアダプタが市販されています。linuxで動くとされるものもあり、たとえばこれなどが鉄板らしいです。

BUFFALO 11n対応 11g/b 無線LAN子機 親機-子機デュアルモード対応モデル WLI-UC-GNM2

BUFFALO 11n対応 11g/b 無線LAN子機 親機-子機デュアルモード対応モデル WLI-UC-GNM2

 ただ、不安定、熱暴走する、アンテナが小さいので通信性能が低い、などの悪評をけっこう聞きます。更に、今回の私のケース、ryzen+linux+USBという構成にはトラブルの臭いしかしません。
 なので、これは選択肢から外しました。

2.PCIwifiカード

 比較的無難そうなのがこれ。USBよりは全然良いと思います。ただ、ドライバがどうなるかわからないのと、PCIに繋ぐ以上他のPCパーツとの相性なども(あまりないとは思いますが)絡んでくるので、情報を集めて慎重にやる必要があります。
 これにするかどうか結構迷いましたが、けっきょく次の選択肢が大差ない値段で実現することがわかったので没にしました。

3.無線LANルータをイーサネットコンバータにする

 これがたったひとつの冴えたやり方です。とりあえず無駄に絵にするとこんな感じです。
f:id:hayataka2049:20170326225525j:plain
 無線は全部無線LANルータがやってくれるので、PC側で無線LANの面倒を見てやらなくても大丈夫というのが一番のポイントです。有線LANのポートがあれば繋がります。
 あとはこれができる無線LANルータを買うだけです。クライアントモード、子機モードといったモードを搭載しているものを選ぶ必要があります。まあ、適当に買えば当たる率の方が高いと思いますが。
 私は下記の記事を参考にしてエレコムWRC-1167GEBK-Iという奴を使いました。4000円しないで買えます(ただしamazonの在庫がこの記事を書いている時点でほとんどありません)。
zigsow.jp

 付属品にCD-Rが付いてきますが、無視してオンラインマニュアルに書いてある方法でブラウザから繋げば10分で設定が終わるくらいの簡単なものです。唯一の欠点は(上の記事でも触れられていますが)LANポートが100メガイーサなことくらいです。もちろん我が家のネットは100Mbpsも出ないので問題ありません。

結論

 特にlinux据え置き機において、無線LANが欲しければルーターを買おう。

xubuntuをcliモードで起動する

 わかるようなわからないような感じだったのでメモ。

 まず、基本的なやり方としては下ので良い。
Ubuntu日本語フォーラム / CUIでの起動、ログインについて

$ sudo systemctl set-default multi-user.target 

 ただし、このコマンドを打ってrebootしてみたら起動中に固まってしまった。たぶん、外部メディアでブートして適当に直せばリカバリできたんだろうけど、そこまで思いつかなかったので、再インストールした(インストール直後にいじってたので、特に失うものがなかった)。
 要は、このコマンドだけ打っても、まだXサーバに仕事を投げるつもりで動いてる奴がいる(grub)ので、そいつを黙らせないといけないんだと思う(細かいことは僕にはわかりません)。黙らせるには、/etc/default/grubを編集。

#GRUB_CMDLINE_LINUX_DEFAULT="quiet splash"
GRUB_CMDLINE_LINUX_DEFAULT="text"

 そしてこのコマンドを打つ。

$ sudo update-grub

 この段階でrebootしたら意図した通りCLIで立ち上がった。後は好きなだけCLIでいじって、GUIがほしくなったらstartxでもすれば良いんだと思う。GUIで立ち上がってほしいなら、上記設定項目を初期状態に戻せば良いものと考えられる。

ryzenマシン作りました

 ryzenのマシンを作りました。1700Xにトマホークという特に面白みのない構成で組んでみました。
 ハード自体は大きな問題もなく立ち上がりましたが、OSのインストールでドはまりしました。そもそも、今回はwindowslinuxデュアルブートにする予定でした。先にwindowsから入れようと思い、学校が配っているwindows10のライセンスで入れようとしましたが、なぜか上手く行きませんでした(同じく学校の配ってるwindows8.1はすんなり入ったので、恐らくハード、特にSATAあたりと、ソフトとの微妙な相性問題に引っかかってしまった気がする。ちょっとお手上げ)。足掻いてもどうにもならなさそうなので、とりあえず先にxubuntuを入れました。
 機械学習やる分にはlinux環境で全く問題ありませんが、日常の作業とかゲームとかどうするのよ・・・