pyhaya’s diary

機械学習系の記事をメインで書きます

AtCoder Beginners Contest 128 C. Switches を解いたのでメモ

久しくブログを更新していなかった気がするので、AtCoder の過去問を解いた記録でも書いてみる。AtCoder Beginners Contest (通称 ABC) 128 の C 問題を解いてみたら再帰関数の良い練習になったのでこれを紹介します。


問題文は下のリンクを参照。

atcoder.jp

解答環境

解く方針

問題文を読んで最初に思ったのが、変数が多いということ。N がスイッチの数で M が電球の数、k が各電球につながっているスイッチの数で、sが各電球につながっているスイッチの番号、pが各電球の点灯条件。。。頭ごっちゃになりそう。変数の持ち方を考えるが、あまり深く考えずに N, M は int、s は多次元ベクトルでまとめて、p もベクトルとして持つことにする。

N, M も大きくなっても 10 までなのでスイッチの状態を全列挙してそれぞれの状態でスイッチが全部つくかどうかを調べることにする。

コード

Step 1

ということで早速、解答の雛形を書いてみる。

#include <bits/stdc++.h>
typedef long long ll;
using namespace std;

int N, M;
vector<vector<int>> info;

int main() {
  // N: switch
  // M: light
  cin >> N >> M;
  vector<int> P(M);

  for (int i = 0; i < M; i++) {
    int k; cin >> k;
    vector<int> tmp(k);
    for (int j = 0; j < k; j++) {
      int a; cin >> a;
      tmp[j] = a - 1;
    }

    info.push_back(tmp);
  }
  for (int i = 0; i < M; i++) {
    cin >> P[i];
  }
  vector<int> init_state(N, 0); // present state of switches
  int res = calc(0, P, init_state);

  cout << res << endl;
}

このコードではデータの読み込みしか書いてない。具体的な計算は calc に丸投げしてる。書くことがあるとすれば電球の番号が1始まりで与えられていて扱いづらいので0始まりにしていることくらい (19 行目)。あとは N, M, info をグローバル変数にしていることくらいか。これは単純に calc にたくさん引数を与えるのが面倒という理由しか無い。

Step 2

calc 関数を書いていく。

int calc(int now_idx, vector<int> P, vector<int> now_state) {
  if (now_idx == N) {
    return is_valid(P, now_state);
  }

  vector<int> new_state;  // state that 'now_idx'th switch is on.
  for (int i = 0; i < now_state.size(); i++) {
    if(i == now_idx) new_state.push_back(1);
    else new_state.push_back(now_state[i]);
  }

  return calc(now_idx + 1, P, now_state) + calc(now_idx + 1, P, new_state);
}

よくある再帰関数の形。main() からは now_state として 0 のみを要素に持つベクトルが与えられる。これは全部のスイッチがoffである状態。スイッチの状態の総数は最初のスイッチが off の場合と on の場合の和になることを利用している。終端条件は、最後のスイッチの on/off を決めた時でこのときに return 1; とすれば単純にスイッチの状態の総数が返るようになる。しかしこの問題では状態に条件がついていて、このスイッチの状態で全ての電球が点灯していなければならない。なのでここでは単純に 1 を返さずにもうワンステップ噛ませる。この判定は is_valid に任せる。

Step 3
int is_valid(vector<int> P, vector<int> state) {
  // P: how to turn on the lights
  map<int, int> search;
  for (int i = 0; i < state.size(); i++) {
    if (state[i] == 1) search[i] = 1;
  }

  vector<int> P_copy;
  copy(P.begin(), P.end(), back_inserter(P_copy));

  for (int i = 0; i < M; i++) {
    for (int j = 0; j < info[i].size(); j++) {
      if (search[info[i][j]]) P_copy[i] = !P_copy[i];
    }
  }

  int count = 0;
  for (int i = 0; i < P_copy.size(); i++) {
    if (P_copy[i] == 0) count++;
  }

  return count == P.size();
}

各電球について、つながっているスイッチの中で on になっている数を数えてそれを2で割ったあまりが点灯条件を満たすか調べる。1つ1つの電球に対してつながっているスイッチの on/off をベクトルから調べるのは面倒なので最初に search というマップにして持つ。

あとは「on になっている数を数えてそれを2で割ったあまりが点灯条件を満たすか調べる」部分だが、これも簡単にかけないか考えると、P の値を スイッチが on であるたびに 0, 1 反転させて 0 になっていればその電球はついていることに気づく。どういうことかというと、ある電球が on になっているスイッチが奇数のときにつくとすると、この電球に対応する p の値は 1 である。この電球につながっている3つのスイッチが on になっているとして p を 3 回反転させると 0 になる。2回なら 1 になる。逆にスイッチの数が偶数のときに電球がつくとするとp = 0 で、2つのスイッチが on になっている状態では p を2回反転させると 0、3つだと 1 になる。つまり電球のつく条件がどちらでも on になっているスイッチの数だけ p の値を反転させるたときに p が 1になっていれば消えているし、0 になっていればついている。これをコードにしているのが 13 行目。

全体のコード

以上のコードをまとめると下のようになる。

#include <bits/stdc++.h>
typedef long long ll;
using namespace std;

int N, M;
vector<vector<int>> info;

int is_valid(vector<int> P, vector<int> state) {
  // P: how to turn on the lights
  map<int, int> search;
  for (int i = 0; i < state.size(); i++) {
    if (state[i] == 1) search[i] = 1;
  }

  vector<int> P_copy;
  copy(P.begin(), P.end(), back_inserter(P_copy));

  for (int i = 0; i < M; i++) {
    for (int j = 0; j < info[i].size(); j++) {
      if (search[info[i][j]]) P_copy[i] = !P_copy[i];
    }
  }

  int count = 0;
  for (int i = 0; i < P_copy.size(); i++) {
    if (P_copy[i] == 0) count++;
  }

  return count == P.size();
}

int calc(int now_idx, vector<int> P, vector<int> now_state) {
  if (now_idx == N) {
    return is_valid(P, now_state);
  }

  vector<int> new_state;
  for (int i = 0; i < now_state.size(); i++) {
    if(i == now_idx) new_state.push_back(1);
    else new_state.push_back(now_state[i]);
  }

  return calc(now_idx + 1, P, now_state) + calc(now_idx + 1, P, new_state);
}

int main() {
  // N: switch
  // M: light
  cin >> N >> M;
  vector<int> P(M);

  for (int i = 0; i < M; i++) {
    int k; cin >> k;
    vector<int> tmp(k);
    for (int j = 0; j < k; j++) {
      int a; cin >> a;
      tmp[j] = a - 1;
    }

    info.push_back(tmp);
  }
  for (int i = 0; i < M; i++) {
    cin >> P[i];
  }
  vector<int> init_state(N, 0); // present state of switches
  int res = calc(0, P, init_state);

  cout << res << endl;
}

Union Find 木の実装

最近 AtCoder の過去問を解いていて、Union Find 木を使うと解くことができる問題に出会いました。これまで Union Find 木は使ったことがなく、聞いたことがあるくらいだったのでこの問題を解くために勉強しました。この記事では Union Find 木がどのようなデータ構造で、どんなことに使えるのかを説明したいと思います。

Union Find 木はどんなデータ構造か

名前にあるように、 Union Find 木は木構造です。つまり、親要素があって、それに連なる子要素がいくつかあるような構造をしています。木構造として有名なものに二分木やそれの特殊なものとしての二分探索木などがありますが、これらに対して Union Find 木は複数の木構造の集合からなるという特徴があります。

同じ木に属している要素は「同じグループ」に属していると解釈されます。つまり Union Find 木は特定の要素が他の要素と同じグループに属しているかどうかを判定するのに適しています

Union Find 木の実装

Union Find 木を C++ で実装してみます。

struct UnionFind {
  vector<int> par;
 
  UnionFind(int N) : par(N) {
    for (int i = 0; i < N; i++) par[i] = i;
  }
 
  int root(int x) {
    if (par[x] == x) return x;
    return par[x] = root(par[x]);
  }
 
  void unite(int x, int y) {
    int rx = root(x);
    int ry = root(y);
 
    if (rx == ry) return;
    par[rx] = ry;
  }
 
  bool same(int x, int y) {
    return root(x) == root(y);
  }
};

UnionFind という名前の構造体を定義します。この構造体は par という要素をもっており、par はそれぞれの要素の親要素をもっています。

初期化

簡単のために考える要素は0 ~ N-1 までの連続する整数であるとして話を進めます。最初に、Union Find 木を初期化するときには全ての要素の親要素が自分自身 (par[i] = i) になるように初期化します。

一番上の親要素の取得

特定の要素の属する木の最も上にいる親要素を取得するメソッドを定義します。par[x] で x と直接つながっている親を取得できるので、これを再帰的にたどっていけば一番上の親に到達できます。

要素の結合

要素 x と要素 y が同じグループに属するとして結合します。x 属する木に y を結合するとして話を進めていきます。これは非常に簡単で、 y の親を x の親にすればよいです。

2つの要素が同じグループに属するか判定

2つの要素の一番上の親が同じかどうかを判定すればいいだけです。


fast.ai をサクッと動かす

機械学習のブーム到来によって、今までに様々なフレームワークが開発されてきました。有名なのは Google が開発している Tensorflow と Facebook が開発している PyTorch ですね。さらにこれらのフレームワークを手軽に試せるようなフレームワークも次々と登場してきました。Tensorflow ですと Keras が Tensorflow をバックエンドに使うようできます。PyTorch をバックエンドに使うフレームワークとして有名なのが fast.ai です。fast.ai は下のようなスローガンを掲げており、なるべく多くの人に深層学習に触れられる機会を与えることを目指していて、実際非常に使いやすいフレームワークです。

Being cool is about being exclusive, and that’s the opposite of what we want. We want to make deep learning as accessible as possible

この記事では、fast.ai の基本的な使い方について紹介したいと思います。

この記事で扱うこと

  • 動物の画像分類
  • ハイパーパラメータの調整
  • モデルの fine tune

データの準備

import fastai2.vision.all as fastai

path = fastai.untar_data(fastai.URLs.PETS)
pets = fastai.DataBlock(
    blocks = (fastai.ImageBlock, fastai.CategoryBlock),
    get_items = fastai.get_image_files,
    splitter = fastai.RandomSplitter(seed=42),
    get_y = fastai.using_attr(fastai.RegexLabeller(r"(.+)_\d+.jpg"), "name"),
    item_tfms = fastai.Resize(460),
    batch_tfms = fastai.aug_transforms(size=224, min_scale=0.75),
)

dls = pets.dataloaders(path/"images")

fast.ai に最初から用意されているペット画像を使って画像分類を試して見ます。fast.ai ではインポートにワイルドカード(*) を使うことが多いようですが、ここではわかりやすさのために fastai.vision.allfastai という名前でインポートします。

読み込むデータは、入力値が画像データ(ImageBlock) で 出力値がカテゴリ(CategoryBlock) です。カテゴリ名はファイル名から正規表現を使って抽出します(RegexLabeller(r"(.+)_\d+.jpg"), "name"))。さらに、画像の前処理としてサイズを揃え(Resize(460))、augmentation をします(aug_transforms(size=224, min_scale=0.75))。

きちんとデータが読み込まれたか確認してみます。

dls.show_batch(rows=2, cols=3)

うまく行っていれば下のように動物の写真が6枚表示されます。

f:id:pyhaya:20200316235953p:plain

また、DataBlock でどレくらいの画像が読み込まれ、どのような処理がなされたかは summary() メソッドを使って確認することができます。今、path には動物画像のデータセットを指定していますが、実際の画像はサブディレクトimages に入っているので summary() メソッドの引数には path/"images" で指定します。

pets.summary(path/"images")

出力が長いので一部を載せると下のような感じになります。

Setting-up type transforms pipelines
Collecting items from ~/.fastai/data/oxford-iiit-pet/images
Found 7390 items
2 datasets of sizes 5912,1478
Setting up Pipeline: PILBase.create
Setting up Pipeline: partial -> Categorize

Building one sample
  Pipeline: PILBase.create
    starting from
      ~/.fastai/data/oxford-iiit-pet/images/american_bulldog_30.jpg
    applying PILBase.create gives
      PILImage mode=RGB size=334x500
  Pipeline: partial -> Categorize
    starting from
      ~/.fastai/data/oxford-iiit-pet/images/american_bulldog_30.jpg
    applying partial gives
      american_bulldog
    applying Categorize gives
      TensorCategory(12)

Final sample: (PILImage mode=RGB size=334x500, TensorCategory(12))


Setting up after_item: Pipeline: Resize -> ToTensor
Setting up before_batch: Pipeline: 
Setting up after_batch: Pipeline: IntToFloatTensor -> AffineCoordTfm -> RandomResizedCropGPU -> LightingTfm

Building one batch
Applying item_tfms to the first sample:
  Pipeline: Resize -> ToTensor
    starting from
      (PILImage mode=RGB size=334x500, TensorCategory(12))
    applying Resize gives
...

訓練してみる

データは読み込めたので、訓練をしてみます。まずは試しに ResNet34 の学習済みモデルをfine tuneしてみます。

learn = fastai.cnn_learner(dls, fastai.resnet34, metrics=fastai.error_rate)
learn.fine_tune(2)

f:id:pyhaya:20200317000647p:plain

pandas の DataFrame を表示するときのようなデザインで各エポックの訓練状況が表示されます。さて訓練結果はどうなっているでしょうか?混同行列を見てみます。

interp = fastai.ClassificationInterpretation.from_learner(learn)
interp.plot_confusion_matrix(figsize=(12, 12), dpi=60)

f:id:pyhaya:20200317000824p:plain

2行のコードで seaborn を使ったときのようなきれいなグラフが表示されます。

ハイパーパラメータの調整

ハイパーパラメータの調整も、fast.ai ではそれを支援する機能があります。

lr_min, lr_steep = learn.lr_find()

この一つのコマンドで、学習率を変化させたときの loss の変化を調べることができます。loss が最も急激に減少する学習率と、最も小さくなる学習率が lr_steep, lr_min に入ります。これと同時に、loss の学習率依存性のグラフが表示されます。
f:id:pyhaya:20200319080522p:plain

モデルの細かい調整

モデルによっては、学習率を層によって変化させたいこともあります。fast.ai ではこのようなことも簡単にできます。

learn.fit_one_cycle(12, lr_max=slice(1e-6, 1e-4))

lr_max にスライスを指定することによって学習率を 1e-6 から 1e-4 まで変化させていくことが可能です。

また、モデルの学習を高速化させ、メモリ消費を抑える手法として混合精度の訓練があります。これも簡単に実現できます。

learn = fastai.cnn_learner(dls, fastai.resnet50, metrics=fastai.error_rate).to_fp16()

最後に、この混合精度モデルで、最初の3エポックは fine tune して、その後に全てのパラメータをフリーにして訓練してみます。

learn.fine_tune(6, freeze_epochs=3)
learn.recorder.plot_loss()

f:id:pyhaya:20200319081714p:plain

まとめ

この記事では、fast.ai の初歩的な使い方をザッと見てきました。データセットの準備や訓練が簡単にできることはもちろん、モデルの調整や訓練結果の確認のためのツールも非常に豊富にもっていることがわかると思います。私もまだ触り始めたばかりですがこれからどんどん使っていきたいと思います。

fastai の Deep Learning 本(Draft)を実行する環境をGCPでそろえる

最近、fastai が深層学習の本のドラフトを GitHub で公開して話題になりました。
github.com

中身を見てみると、Jupyter Notebook 形式で書かれており、説明を読みながらその場で実行することが可能になっておりすごく便利そうです。この記事では、この良質な教材を使って深層学習を勉強するための環境構築について書きます。

構築する環境の概要

深層学習を学ぶにあたって一番のボトルネックとなるのがマシンスペックです。学習が進むにつれて GPU がなければ実行が遅すぎる、もしくはメモリが足りなくてそもそも実行できないという例が出てきます。

実際に教材のイントロダクションにも以下のように GPU を用いることが強く推奨されています。

01_intro.ipynb

Getting a GPU deep learning server

To do nearly everything in this book, you'll need access to a computer with an NVIDIA GPU (unfortunately other brands of GPU are not fully supported by the main deep learning libraries). However, we don't recommend you buy one; in fact, even if you already have one, we don't suggest you use it just yet! Setting up a computer takes time and energy, and you want all your energy to focus on deep learning right now. Therefore, we instead suggest you rent access to a computer that already has everything you need preinstalled and ready to go. Costs can be as little as US$0.25 per hour while you're using it, and some options are even free.

ここにも書いてあるように NVIDIA GPU を買ってくる必要はなく、クラウドサービスを利用する方が簡単に、かつ安く環境を構築することができます。私は NVIDIA の RTX 2080 をもっているので最初はこれでやろうと思っていたのですが結構最初のところでメモリが足りないと怒られたのでクラウドサービスを利用することにしました。そこでこの記事では以下のような環境構築を行います。

環境構築

下準備

では早速、環境構築を行っていきます。GCP を使うので課金を有効にしておきます。
cloud.google.com

また、GPU も使うので GPU の割り当ての設定も確認しておきます。
cloud.google.com

何もしていない状況だと GPU の最大割り当て数が 0 になっていると思うのでこうなっている場合には上のリンクに書いてある手順で増やしてもらえるように申請します。

インスタンスの作成

これらの設定を行った後には、インスタンスの作成を行っていきます。GCP のページの左メニューから「Compute Engine」-> 「VM インスタンス」を選びます。
f:id:pyhaya:20200304081754p:plain

VM インスタンスのページの左下に「Marketplace」があると思うので、そこをクリックして出てきたウィンドウ内で「deep learning vm」を検索します。するとこんな感じのものが検索にかかると思います。
f:id:pyhaya:20200304082156p:plain

「LAUNCH」 をクリックしてインスタンスの作成画面に遷移すると、マシンスペック等を調整する画面が出てきます。こんな感じです。

f:id:pyhaya:20200304082401p:plain

名前を適当にセットして、GPU の種類を選択します。どれを使うかは財布と相談しましょう(Tesla T4 あたりがいいかも)。1か月の推定料金は右に出ますがこれは24時間、1か月の間インスタンスを使い続けた場合の料金みたい(?)なので個人的な勉強用途の場合にはもう少し安くなると思います。画像にあるような Tesla K80 を使った場合には 一時間料金は $0.4 らしいので一日2時間、週5日使うとおおよそ $16 くらいかと思います。CPUやリージョンの変更もできますが、CPU の構成やリージョンによっては特定の GPU を使えない場合もあるので注意しましょう。

Framework はこの fastai の教材の勉強用には 「PyTorch 1.3 + fast.ai (CUDA 10.0)」を選びましょう。ただし教材では 「PyTorch 1.4」を使うのでインスタンス作成後にアップデートします(後述)。

下の方を見ていくといろいろと書いてありますが、よく見ると「Install NVIDIA GPU driver automatically on first startup?」と書いてあるチェックボックスがあります。これにチェックを入れておけばインスタンス作成時に面倒な GPU のドライバインストールを済ませておいてくれるのでチェックを入れておくことを推奨します。

すべての設定が終わったら「デプロイ」をクリックしてインスタンスを作成します。しばらく待てばインスタンスの作成は終了です。

環境の設定

ここからは Cloud SDK を使ってローカルから インスタンスSSH 接続します。 Cloud SDK のインストール等は以下のリンクを参照してください。

cloud.google.com


接続する際には、ローカルの 8080 番ポートに インスタンスの 8080 番ポートを接続するようにしてみます。

gcloud compute ssh --zone $ZONE $INSTANCE_NAME -- -L 8080:localhost:8080

$ZONE$INSTANCE_NAME に関しては自分で作ったインスタンスのものに置き換えてください。接続できたら、シェルで

jupyter lab

と打ちます。すると Jupyter Lab が立ち上がりますので ローカルで https://localhost:8080 にアクセスしてみて Jupyter Lab のページが表示されるか確認してください。

では、ここから fast.ai の教材のためのセットアップをしていきます。インスタンスに Git は最初から入っているので、Jupyter Lab からシェルを開いて、

git clone https://github.com/fastai/fastbook.git
cd fastbook

としてクローンします。そうしたら必要なパッケージを入れていきます。最初に注意しなければいけないのは、このインスタンスには nvidia-ml-py3 というパッケージが無いことです。これは普通 CUDA を入れると標準で入ってくるものらしいのですが、 GCPインスタンスにはこれが入っていないようです。これを入れたあとにはまず、PyTorch を 1.4 にアップグレードし、その後に残りの必要なパッケージを入れていくという手順で行きます。まとめると、

pip install nvidia-ml-py3
pip install -U torch
pip install -r requirements.txt

これで完了です!

ここまでくれば、あとは普段 Jupyter Lab を使うときと同じです。インスタンスを起動している間は課金の対象になるのでしばらく使わないときには停止をするのを忘れないようにしてください。では、深層学習の勉強をがんばりましょう!!

Rust で テキストファイルからデータを読み出す Python 拡張を書いたら爆速だった話

普段、実験をしていて得られるデータを np.loadtxt で読んでいるが、ためしにこの部分を Rust で書いて Python から呼び出してみたら速かったという話を書きます。


最近こんな記事を書きました。
pyhaya.hatenablog.com
この記事では、Rust でテキストデータを読み込んだときにどれくらいの速度が出るのだろうということを試してみた記事です。この中で、最後に Python の numpy との比較をしているのですが、ここで結構 Rust と numpy の間に差ができており、Rust で Python 拡張を書いたときにどれくらいスピードが出るのか気になりました。

Rust から Python 拡張を書くのはそんなに難しくないので、試してみた結果をここに書くことにしました。

環境

  • Ubuntu 19.10
  • Cargo 1.39.0-nightly (<- stable だと動かないので注意)
  • Python 3.7.3

rustup を入れていれば nightly は以下の記事のように簡単にインストールすることができます。
qiita.com

準備

適当なディレクトリで次のコマンドを打ちます。("speed" はディレクトリの名前)

cargo new --lib speed

生成されたディレクトリ内の Cargo.toml を修正する。

[package]
name = "speed"
version = "0.1.0"
authors = ["Your name <mail address>"]
edition = "2018"

[lib]
name="speed"
crate-type=["cdylib"]

[dependencies]

[dependencies.pyo3]
version="0.8"
features=["extension-module"]

コード

Numpy と同じく、loadtxt() という名前の関数を定義して Rust で書き下します。このとき、返り値は PyResult型である必要があることに注意します。

src/lib.rs

use pyo3::prelude::*;
use std::fs::read_to_string;

#[pymodule(speed)]
fn loadtxt(py: Python, m: &PyModule) -> PyResult<()> {
    #[pyfn(m, "loadtxt")]
    fn loadtxt(_py: Python, filename: &str) -> PyResult<(Vec<f32>, Vec<f32>)> {
        let data = read_to_string(filename);
        let xy = match data {
            Ok(content) => content,
            Err(error) => { panic!("Could not open file: {}", error); }
        };

        let xy_pairs: Vec<&str> = xy.trim().split("\n").collect();
        let mut x: Vec<f32> = Vec::new();
        let mut y: Vec<f32> = Vec::new();

        for pair in xy_pairs {
            let p: Vec<&str> = pair.trim().split(" ").collect();

            x.push(p[0].parse().unwrap());
            y.push(p[1].parse().unwrap());
        }

        Ok((x, y))
    }

    Ok(())
}

これをビルドすれば、Python側から呼び出せるバイナリデータが出来上がります。

cargo build --release

速度比較

target/release 内に含まれる libspeed.sospeed.so に名前を変えてわかりやすいように別のディレクトリにコピーします。そのディレクトリで Python ファイルを作って速度を比較してみました。


まず、読み出すファイルを作成します。999999行のデータです。

data = ""
num = 1000000
for i in range(num):
    data += f"{i} {i*2}"
    if i < num - 1:
        data += "\n"
with open("example.txt", "w") as f:
    f.write(data)

では、実際に速度を比較してみます。

from speed import loadtxt
import numpy as np
import time

if __name__ == "__main__":
    filename = "example.txt"
    
    # Rust
    t1 = time.perf_counter()
    x, y = loadtxt(filename)
    print("Rust time: ", time.perf_counter() - t1)

   # Numpy
    t2 = time.perf_counter()
    x, y = np.loadtxt(filename).T
    print("Numpy time: ", time.perf_counter() - t2)
Rust time:  0.29457211199951416
Numpy time:  3.338321666000411

Rust で書いたほうは Numpy よりも 10 倍以上速いという結果になりました。

まとめ

この記事では、テキストデータを Python で読み出すためにNumpyのloadtxtを使った場合と、Rustで自作の拡張を作った場合の2通りのコードを書き、速度の比較を行いました。その結果、Rustのほうが 10 倍以上速いという結果になりました。ただ、Rust の方は読み出すデータが2列からなるということを知った上で書いてあるので、そのせいで Numpy より早くなっているという可能性はあるかも知れません。今後はそこらへんも検証していきたいと思っています。

実践Rust入門[言語仕様から開発手法まで]

実践Rust入門[言語仕様から開発手法まで]

Dell XPS 13 7390 のレビュー

XPS 13 7390 が届いたのでざっくりとレビューしてみます。外見の比較対象として今まで使っていた Dell Inspiron 13 7000 series を出しています。

(2019/09/29 追記)ストレージテストと Core-i7 10510U のパフォーマンステストの結果を載せました

仕様

  • Windows 10 Home
  • Intel Core-i7 10510U
  • RAM 16 GB
  • 日本語キーボード (残念ながらUS配列は2 in 1でないと選べない...)

2 in 1 ではないほうです。一応リンク貼りますが、アフィリエイト記事ではないので書きたいこと書きます。
www.dell.com

見た目

見た目はすごく格好いいです。Dell のパソコンはヒンジが強くて開けづらいという問題がありましたが、このパソコンは少し改善されています。「片手で開くことが可能」ということをHPで書いてあるように片手で開いてもキーボード側がついてくるということはありません。ただし最初の60 度くらいまでは。。。ある程度開くとキーボードが付いてきます、残念ながら Mac のようにはいきません。

世界最小の13.3インチとうたっているだけあって、本当に13.3型かと疑うほど小さいです。下の写真は、これまで私が使ってきた Dell Inspiron 13 との2ショットです。ディスプレイサイズは同じなのに外形はこんなにも違います。

f:id:pyhaya:20190928142958j:plain

厚みは滑り止めがそこそこ分厚いので Inspiron と同じくらいで、最小で 1.4 mm, 最厚で 1.8 mm といったところです。重さは 1.2 kg くらいなので持ち運びで重すぎるということはなさそうに思います(ちなみに Inspiron は 1.65 kg あったのでちょっと辛かった...)。

キーボードを Inspiron と比べてみます。下は Inspiron のキーボードです。
f:id:pyhaya:20190928143808j:plain
見たらわかりますが、キーボードの外側に枠のようなものがあって Enter や Backspace を打つときにこれに阻まれて打てないということがありました。
f:id:pyhaya:20190928144026j:plain
しかし XPS ではそれがなくなり、さらに Enter が大きくなったので打ちやすくなりました。また、キー間の隙間も 4 mm -> 3 mm になって隙間に引っかかるということもなくなりました。

排熱・ファン

ファンの音は Inspiron と比べて大きいです。初期セットアップでいろいろインストールしてた時にはずっとフル回転でひどかったです。それ以外の時でも電源につないでいるときは若干ファンの音がします。図書館など静かなところで聞こえるかなくらいですが。電源ケーブルから外すと基本的には静かです。

あと、これは謎なのですが、ファンが少しでも動いた状態でスリープにすると、一瞬だけファンがフル回転して静かになります。これが原因で一度交換してもらったのですが(この時は一瞬フル回転が何回も連続して生じていた)、新しく来たものも同じだったので(回数が1回になったので改善した?)仕様かと思います。調べたら BIOS のアップデートで直るということも書いてあり、試したのですが効果はありませんでした。

パフォーマンステスト

CrystalDiskMark を使ってディスクのパフォーマンス、CineBench, PassMark を使って CPU 性能の評価をしてみました。

CrystalDiskMark

f:id:pyhaya:20190929163743p:plain

NVMe SSD を積んでいるので、それなりの結果が出たと思います。HDD や SATA SSD で計測したときの結果は以下のサイトが詳しいです。
xn--pc-mh4aj6msdqgtc.com

CineBench

f:id:pyhaya:20190929163948p:plain

CineBench は有名ですが、最近 Release 15 から Release 20 になったばかり(?) のせいか、比較対象があまりありませんでした。8世代 i5 の U タイプは 1500 ほどらしいです。

PassMark

f:id:pyhaya:20190929164317p:plain

PassMark はこんな感じ。上の CineBench とともに最近の Intel CPU のベンチマークを載せているサイトがあったので貼っておきます。
https://thehikaku.net/pc/other/corei7-1065g7.html

これと比較すると、CPU 性能は Core-i7 1065G7 よりよいようです。(多分 グラフィックス性能は負ける)

まとめ

ここまで 10 世代 Intel CPU を積んだ XPS 13 7390 のレビューをかなりざっくりしてきました。パフォーマンス計測などは行わず、主に外見のレビューをしてきましたが、まあまあ良いパソコンだと思いました。ファンの音を重要視する人は少し注意したほうが良いと思いましたが、デザイン・操作性に関しては文句がないです。

最後に、性能に関しては 2 in 1のほうがより高いものを選ぶことが可能です。グラフィックス性能を重要視する人(Ice Lake の CPU が欲しい人)、それに関連してメモリを 32 GB ほしい人は 2 in 1を選ぶべきだと思います。あと、2 in 1 では US 配列キーボードを選ぶことが可能なので US 配列が欲しい人もこっちにするべきです。(あれ?これってみんな 2 in 1を買うべきなのでは...?)

(速度比較) Rust でテキストファイルからデータを読み出す

Rust を使って、テキストファイルを開いて中のデータを読み出すということをやってみます。Rust では読み込み方法がいくつもあるのでそれの比較と、普段私が使っている Pythonnumpy.loadtxt との比較も行っていきます。

環境

  • Rust 1.39.0-nightly (nightly でなくても動く)
  • Windows 10 (Intel Corei7 10510U , RAM 16GB)
  • ( Python 3.7.3 )

読み込むデータ

時間を計測したいので、そこそこ大きなテキストファイルを用意します。ここは慣れている Python で行いました。

data = ""
num = 1000000
for i in range(num):
    data += f"{i} {i*2}"
    if i < num - 1:
        data += "\n"

with open("example.txt", "w") as f:
    f.write(data)

これを実行すると、タブで区切られた100万行2列のテキストデータが生成されます。

Rust でテキストファイルを読み込む

最初に、Rust を使ってこのテキストファイルを読み込んでx, y という名前の2つのベクトルにデータを入れるコードを書いてみます。

read_to_string を使う方法

まずは標準ライブラリに入っている関数の一つであるread_to_string を使って書いてみます。

use std::fs::read_to_string;

pub fn open_file(filename: &str) -> (Vec<f32>, Vec<f32>){
    let data = read_to_string(filename);
    let xy = match data {
        Ok(content) => content,
        Err(error) => {panic!("Could not open or find file: {}", error);}
    };

    let xy_pairs: Vec<&str> = xy.trim().split("\n").collect();
    let mut x: Vec<f32> = Vec::new();
    let mut y: Vec<f32> = Vec::new();

    for pair in xy_pairs {
        let p: Vec<&str> = pair.trim().split(" ").collect();

        x.push(p[0].parse().unwrap());
        y.push(p[1].parse().unwrap());
    }

    (x, y)
}

時間を計測してみると、

time cargo run --release

real    0m0.248s
user    0m0.156s
sys     0m0.047s

BufReader を使う方法

Rust には std::io にもファイル読み込みができる関数が用意されているのでこっちを使ってみます。

use std::fs::File;
use std::io::{BufReader, BufRead};

pub fn open_file(filename: &str) -> (Vec<f32>, Vec<f32>){
    let f = File::open(filename).unwrap();
    let buf = BufReader::new(f);
    let mut x: Vec<f32> = Vec::new();
    let mut y: Vec<f32> = Vec::new();

    for line in buf.lines() {
        let l: &str = &line.unwrap();
        let p: Vec<&str> = l.trim().split(" ").collect();

        x.push(p[0].parse().unwrap());
        y.push(p[1].parse().unwrap());
    }
    (x, y)
}

時間は

time cargo run --release

real    0m0.264s
user    0m0.188s
sys     0m0.047s

遅くなった(?)。

split_whitespace を使う方法

一番最初の方法では trim().split(" ") と、いちいち空白文字で分けるということをこっちで指定して分割を行っていました。しかし、Rust にはそれをやるための関数 split_whitespace が用意されているのでこっちを使ってみます。これは一番最初のコードでtrim().split(" ") の部分を split_whitespace() に書き換えるだけです。

時間を計測すると、

time cargo run --release

real    0m0.265s
user    0m0.188s
sys     0m0.063s

あんまり変わらない。

文字列から浮動小数点数への変換にlexical を使う

このコードをプロファイリングしてみるとわかるのですが、実行時間の40 % が 文字列処理で取られています。ファイルの読み込みとは少し話がずれますが、ここも別の方法を試してみます。&str から f32 への変換にここまでのコードでは parse() を使ってきました。しかし調べてみると、どうやらlexical というクレートが高速変換を謳っているということが分かりました。これを試してみます。

docs.rs

use lexical; をしたあとに次の部分を変更します。

// before
        x.push(p[0].parse().unwrap());
        y.push(p[1].parse().unwrap());

// after
        x.push(lexical::parse(p[0]).unwrap());
        y.push(lexical::parse(p[1]).unwrap());
time cargo run --release

real    0m0.789s
user    0m0.516s
sys     0m0.203s

今までで一番遅いですね。。。(なんで?)

(おまけ) numpy.loadtxt との比較

最後に、Numpy を使ったときの Python との比較をしてみます。

import numpy as np

x, y = np.loadtxt("example.txt").T

超簡単!! やはり Python で書くとシンプルですね。さて、時間は

real    0m13.730s 
user    0m12.297s
sys     0m1.438s

遅い。ちなみにimport numpy as npの部分は 0.39 secくらいなのでインポートに時間が食われているわけではない。さすがに Numpy でもテキスト読み込み処理に関してはそんなに早くないのか?

まとめ

  • read_to_string が一番速かった
  • Python より全然速い!
  • もっと速くする方法知りたい(&str -> f32 とか特に)

実践Rust入門[言語仕様から開発手法まで]

実践Rust入門[言語仕様から開発手法まで]