2018/12/14


先日、mopp さんが Vim に flatten() を追加するプルリクエストを追加してくれたのだけど、その時の記憶を整理する為に書く自分の為の記事。

add flatten() to flatten list by mopp - Pull Request #3676 - vim/vim - GitHub

I'm a bit confused by the maxdepth argument. I would expect it to specify the maximum depth of the r...

https://github.com/vim/vim/pull/3676

Vim script のリストは以下の様に、異なる型が混在できる。Ruby や他のスクリプト言語でも一般的。そしてスクリプト言語には一般的にリストを平坦化する為の flatten という関数ないしはメソッドが用意されている。

let foo = [12, ["bar"], 3]
Vim本体に組み込み関数を追加するパッチを投げた - Qiita

Vim本体に手を加える 次に本体への修正ですが、大体1週間くらいで出来ました。 しかし、これは私一人の力ではなく、7割りくらい vim-jp のおかげです。 vim-jpは日本のVim開発者(Plug...

https://qiita.com/mopp/items/084abe28681202bda30e

mopp さんが Advent Calendar でその時の様子を書いてくれているんだけど、flatten() ははじめ再帰を使って書かれていた。途中で僕が「ループに直したらこうなる」という感じにコードを貼ってしまったので、後でループに直す実装を楽しみにしていた mopp さんには悪い事をしてしまった。申し訳ない。ぜひ次は flat_map() を実装して下さい。その時考えていたのだけど、flatten() の様な関数を再帰でなくループにするには本来ならばスタックに相当する何かが必要になるはずだろうと踏んでいた。なぜならリストを再帰降下するという事は戻り場所を知っておく必要があり、ループに直すのであればそれ相当のスタック配列が必要だと考えていたからだ。そして一般的にはこのスタック配列の実装がめんどくさいので皆再帰を使ってお茶を濁そうとする。僕もよくやる。

例えば以下の様な簡単なリスト構造を作ってみたとする。

#include <stdio.h>
#include <stdlib.h>
#include <string.h>

typedef enum _type_t {
  INT_T,
  STRING_T,
  LIST_T,
} type_t;

typedef struct _value_t {
  int ref;
  type_t type;
  union {
    int n;
    char *s;
    struct _list_t *l;
  } val;
} value_t;

typedef struct _item_t {
  struct _item_t *next;
  value_t *value;
} item_t;

typedef struct _list_t {
  item_t *first;
} list_t;

static void
free_value(value_t *v) {
  item_t *i;

  if (v->ref-- > 0return;

  switch (v->type) {
  case INT_T:
    break;
  case STRING_T:
    free((void*)v->val.s);
    break;
  case LIST_T:
    i = v->val.l->first;
    while (i != NULL) {
      item_t *next = i->next;
      free_value(i->value);
      free(i);
      i = next;
    }
    free((void*)v->val.l);
    break;
  }
  free((void*)v);
}

static value_t*
new_list_value() {
  value_t *v = malloc(sizeof(value_t));
  v->type = LIST_T;
  v->val.l = malloc(sizeof(list_t));
  v->val.l->first = NULL;
  v->ref = 0;
  return v;
}

item_t*
new_item(value_t *v) {
  item_t *item = malloc(sizeof(item_t));
  item->value = v;
  item->next = NULL;
  ++v->ref;
  return item;
}

static void
list_add_value(list_t *list, value_t *rhs) {
  item_t *i = list->first;

  if (i == NULL) {
    list->first= new_item(rhs);
    return;
  }

  while (i->next != NULL) {
    i = i->next;
  }

  i->next= new_item(rhs);
}

static void
list_insert_value(list_t *list, item_t *before, value_t *value) {
  item_t *next;
  
  if (before == NULL) {
    list->first= new_item(value);
    return;
  }

  next = before->next;

  before->next= new_item(value);
  before->next->next = next;
}

static void
list_remove_item(list_t *list, item_t *item) {
  item_t *i, *before = NULL;
  
  for (i = list->first; i != NULL; i = i->next) {
    if (i == item) {
      if (before == NULL)
        list->first = i->next;
      else
        before->next = i->next;

      free_value(item->value);
      free(item);
      return;
    }
    before = i;
  }
}

static value_t*
new_int_value(int n) {
  value_t *v = malloc(sizeof(value_t));
  v->type = INT_T;
  v->val.n = n;
  v->ref = 0;
  return v;
}

static value_t*
new_string_value(const char* s) {
  value_t *v = malloc(sizeof(value_t));
  v->type = STRING_T;
  v->val.s = strdup(s);
  v->ref = 0;
  return v;
}

static void
print_value(value_t *v) {
  item_t *i;

  switch (v->type) {
  case INT_T:
    printf("%d", v->val.n);
    break;
  case STRING_T:
    /* TODO: escape non-printable */
    printf("\"%s\"", v->val.s);
    break;
  case LIST_T:
    printf("[");
    for (i = v->val.l->first; i != NULL; i = i->next) {
      print_value(i->value);
      if (i->next) printf(", ");
    }
    printf("]");
    break;
  }
}

int
main(int argc, char* argv[]) {
  value_t *list, *sub;

  list = new_list_value();  
  list_add_value(list->val.l, new_int_value(1));
  list_add_value(list->val.l, new_string_value("foo"));

  sub = new_list_value();  
  list_add_value(sub->val.l, new_string_value("bar"));
  list_add_value(list->val.l, sub);

  list_add_value(list->val.l, new_int_value(3));

  print_value(list);
  free_value(list);
  return 0;
}

数値と文字列とリストが扱える物。リストの中にはそのいずれかを混入できる。お気持ち程度の参照カウンタを入れてあるが動くかどうか確認してないし本題はそこじゃない。これを実行すると以下の様に表示される。

[1, "foo", ["bar"], 3]

このリスト関数を使って flatten() を実装する場合、簡単に思い付くのが再帰を使った以下の方法。vital.vim でも再帰を使ってる。

functions:flatten(list, ...) abort
  let limit = a:0 > 0 ? a:1 : -1
  let memo = []
  if limit == 0
    return a:list
  endif
  let limit -= 1
  for Value in a:list
    let memo +=
          \ type(Value) == type([]) ?
          \   s:flatten(Value, limit) :
          \   [Value]
    unlet! Value
  endfor
  return memo
endfunction

上記のリスト関数を使った場合だと以下の様になる。

static void
flatten_list(item_t *before, list_t *list) {
  item_t *i, *j, *prev = NULL;

  for (i = list->first; i != NULL; i = i->next) {
    if (i->value->type == LIST_T) {
      flatten_list(i, i->value->val.l);
      list_remove_item(list, i);
      return;
    }
    if (before != NULL) {
      list_insert_value(list, before, i->value);
      before = before->next;
    }
  }
}

リストを舐めながら要素がリストだったら挿入位置とそのリストを引数に要素を追加する関数(自身)を呼び出す。呼び出したあと元々リストがあった箇所を削除する。この flatten() は再帰を使ってるのでメモリを多く消費するしスタックオーバーフローで突然死してしまう可能性がある。でも良く考えると flatten() は、現在いる要素の子要素がリストの場合、そのリストの中身をすべて現在いる場所に移動し、自身を削除し、そして今と同じ箇所で再検査すればいいだけなのだ。スタックとして覚えておく必要もない。なので実装は以下の様になる。

static void
flatten_list(list_t *list) {
  item_t *i, *j, *prev = NULL;

  for (i = list->first; i != NULL; i = i->next) {
    if (i->value->type == LIST_T) {
      item_t *before = i;
      for (j = i->value->val.l->first; j != NULL; j = j->next) {
        list_insert_value(list, before, j->value);
        before = before->next;
      }

      if (prev == NULL)
        list->first = i->next;

      prev->next = i->next;
      i = prev;
    }
    prev = i;
  }
}

言ってみるならば、自分がネストに降下するんじゃなく、flat にする事でネストがこっちに上がってくるという事。実行すると以下が表示される。

[1, "foo", "bar", 3]

再帰は消え、スタックオーバーフローの心配もなくなり、メモリの消費量も減り、良い事づくめだ。逆に、今まで何度か flatten() は書いた事があるけどなぜ自分はこれまで flatten() を再帰で作ろうと考えてしまったのかと思い起こしたくもなった。

追記 この最適化方は自身のリストが破壊されるから出来る方法なので、flatten 済みのリストをを返す場合には使えないので注意。

Posted at by



2018/11/08


gobrain という Golang だけで実装されたニューラルネットワークを見つけたので遊んでみました。

GitHub - goml/gobrain: Neural Networks written in go
https://github.com/goml/gobrain

作りもシンプルですし、扱い方も簡単なのでちょっとしたサンプルを書くのには向いてると思います。例えば FizzBuzz であればこんな感じ。

package main

import (
    "math/rand"

    "github.com/goml/gobrain"
)

type FizzBuzz []float64

func (fizzbuzz FizzBuzz) Type() int {
    for i := 0; i < len(fizzbuzz); i++ {
        if fizzbuzz[i] > 0.4 {
            return i
        }
    }
    panic("Sorry, I'm wrong")
}

func teacher(n int) []float64 {
    switch {
    case n%15 == 0:
        return []float64{1000}
    case n%3 == 0:
        return []float64{0100}
    case n%5 == 0:
        return []float64{0010}
    default:
        return []float64{0001}
    }
}

func bin(n int) []float64 {
    f := [8]float64{}
    for i := uint(0); i < 8; i++ {
        f[i] = float64((n >> i) & 1)
    }
    return f[:]
}

func main() {
    rand.Seed(0)

    // make patterns
    patterns := [][][]float64{}
    for i := 1; i <= 100; i++ {
        patterns = append(patterns, [][]float64{
            bin(i), teacher(i),
        })
    }

    ff := &gobrain.FeedForward{}

    // 8 inputs, 100 hidden nodes, 4 outputs
    ff.Init(81004)

    // epochs: 1000
    // learning rate: 0.6
    // momentum factor: to 0.4
    ff.Train(patterns, 10000.60.4false)

    for i := 1; i < 100; i++ {
        switch FizzBuzz(ff.Update(bin(i))).Type() {
        case 0:
            println("FizzBuzz")
        case 1:
            println("Fizz")
        case 2:
            println("Buzz")
        case 3:
            println(i)
        }
    }
}

今日はこの gobrain を使って画像分類を作ってみました。特徴抽出やノーマライズはやってないので実用的ではない事に注意下さい。

まず flickr 等から薔薇とユリと向日葵の画像を貰ってきて下さい。

薔薇
薔薇
ユリ
ユリ

刺青混じってませんか...

向日葵
向日葵

各20毎程度で構いません。次に画像を読み込んで3チャネルに分割します。

func decodeImage(fname string) ([]float64error) {
    f, err := os.Open(fname)
    if err != nil {
        return nil, err
    }
    defer f.Close()

    src, _, err := image.Decode(f)
    if err != nil {
        return nil, err
    }

    bounds := src.Bounds()
    w, h := bounds.Dx(), bounds.Dy()
    if w < h {
        w = h
    } else {
        h = w
    }
    bb := make([]float64, w*h*3)
    for y := 0; y < h; y++ {
        for x := 0; x < w; x++ {
            r, g, b, _ := src.At(x, y).RGBA()
            bb[y*w*3+x*3= float64(r) / 255.0
            bb[y*w*3+x*3+1= float64(g) / 255.0
            bb[y*w*3+x*3+2= float64(b) / 255.0
        }
    }
    return bb, nil
}

これで画像データが1次元の float64 配列になりこれが入力となります。これに薔薇やユリや向日葵のラベルを紐づけるためにラベルの添え字番号を使い同じ様に float64 配列にする関数を作ります。

func bin(n int) []float64 {
    f := [8]float64{}
    for i := uint(0); i < 8; i++ {
        f[i] = float64((n >> i) & 1)
    }
    return f[:]
}

func dec(d []float64int {
    n := 0
    for i, v := range d {
        if v > 0.9 {
            n += 1 << uint(i)
        }
    }
    return n
}

あとは gobrain を初期化して学習させれば推論器が出来上がるのですが

ff.Init(len(patterns[0][0]), 40len(patterns[0][1]))
ff.Train(patterns, 10000.60.4false)

gobrain は Pure Go という事もあり struct をそのまま JSON にエンコードしてやればこれがモデルファイルになる事に気付きました。

func loadModel() (*gobrain.FeedForward, []stringerror) {
    f, err := os.Open("labels.txt")
    if err != nil {
        return nilnil, err
    }
    defer f.Close()

    labels := []string{}
    scanner := bufio.NewScanner(f)
    for scanner.Scan() {
        labels = append(labels, scanner.Text())
    }
    if scanner.Err() != nil {
        return nilnil, err
    }

    if len(labels) == 0 {
        return nilnil, errors.New("No labels found")
    }

    f, err = os.Open("model.json")
    if err != nil {
        return nil, labels, nil
    }
    defer f.Close()

    ff := &gobrain.FeedForward{}
    err = json.NewDecoder(f).Decode(ff)
    if err != nil {
        return nil, labels, err
    }
    return ff, labels, nil
}

func makeModel(labels []string) (*gobrain.FeedForward, error) {
    ff := &gobrain.FeedForward{}
    patterns := [][][]float64{}
    for i, category := range labels {
        bset, err := loadImageSet(category)
        if err != nil {
            return nil, err
        }
        for _, b := range bset {
            patterns = append(patterns, [][]float64{b, bin(i)})
        }
    }
    if len(patterns) == 0 || len(patterns[0][0]) == 0 {
        return nil, errors.New("No images found")
    }
    ff.Init(len(patterns[0][0]), 40len(patterns[0][1]))
    ff.Train(patterns, 10000.60.4false)
    return ff, nil
}

func saveModel(ff *gobrain.FeedForward) error {
    f, err := os.Create("model.json")
    if err != nil {
        return err
    }
    defer f.Close()
    return json.NewEncoder(f).Encode(ff)
}

全体のソースは GitHub に置いてあります。

GitHub - mattn/flower-detect
https://github.com/mattn/flower-detect

実際に試してみます。

test

結果は「sunflower」。そうだよ向日葵だよ。

test

結果は「rose」。そうだよ薔薇だよ。

test

結果は「lilium」。そうだよユリだよ。

gobrain を JSON で出力してモデル扱いにするというこの方法を使えば、簡単な画像分類であればインストールが難しい TensorFlow を使わずともポータブルに実行出来ます。特に GPU を使う程ではないといった場合にも便利かなと思います。一応 smartcrop というパッケージを使って画像内で注目される部分で crop する様にしてありますが、いくらかの画像では失敗します。これは画像をノーマライズしていないのでしょうがないですね。学習には10分くらい掛かると思います。

尚 Golang で TensorFlow やりたい人は以前書いた記事の方を参照下さい。

Big Sky :: golang で tensorflow のススメ

tensorflow といえば Python と思っておられる方も多いのではないでしょうか。間違いではないのですが、これは初期に作られた Python 向けのバインディングに研究者達が多く食いついた結...

https://mattn.kaoriya.net/software/lang/go/20180825013735.htm
Go言語による並行処理 Go言語による並行処理
Katherine Cox-Buday, 山口 能迪
オライリージャパン 単行本(ソフトカバー) / ¥3,080 (2018年10月26日)
 
発送可能時間:

Posted at by



2018/10/22


O'Reilly Japan, Inc. 様に献本頂きました。ありがとうございます。

そして献本頂く際にお声を掛けて頂いた、本書の翻訳を担当された ymotongpoo さんにもお礼を申し上げます。ありがとうございます。

本書の訳は非常に素晴らしく、とても原文が英文であったとは思えないほど綺麗で、読んでいく中で「原文でどの様に表現されているんだろう」といった引っかかりも無く、とてもスムーズに読み進められました。

Go 言語に関わって随分と長くなってきました。初めて Go を知ってからユーザがどんどん増える様を見る事が出来るのは正直に言って非常に嬉しいです。

ふと Go の魅力は何かと聞かれたら幾つか挙げる事が出来ますが、間違いなく選ぶのが「非同期処理の簡単さ」です。これまで多くの開発者が OS スレッドで実現してきた非同期処理を、Go 言語は少ないイディオムとインテリジェントなランタイムを使って誰でも簡単に非同期処理を実装できる仕組みを提供しています。

一般的に非同期処理の実装が難しいと言われるのには理由があります。それは直面している、ボトルネックがどのパターンで発生しているのか、また解決するにはどの手法を選ぶかの選択肢が多すぎるのです。

どこを関数に切り出してスレッドにし、どうやってデータを受け渡し、どうやってスレッドを待合せたら良いのか、慣れたプログラマでもそこそこ難しい問題です。

もしあなたが誰かに仕事を依頼し、その間に自分の仕事をしたかったとしましょう。「これで自分の仕事に集中できる」そう思うかもしれません。しかし依頼した仕事が終わればそれを手元の作業に取り込みたいとも思うでしょう。どうやって依頼した仕事の完了を待ったら良いでしょうか?都度、仕事が終わってないか確認しますか?それとも依頼した人に仕事の完了を伝えて貰いますか?もしかすると依頼した人には小まめにヒントを与える必要があるかも知れません。やりかたは様々です。一番効率の良い手法を選ばなければなりません。

Go の非同期処理は CSP (Communicating Sequential Processes) という並行処理の為の設計理論を元にしており、同期型のメッセージパッシングを使う事で CPU の負荷を抑えつつ各非同期処理を分かりやすく実現しています。このメッセージパッシングが goroutine と channel という言語レベルで記述可能というのが Go の特徴です。例えば channel を使って入力を2つに分ける tee を実装するのであれば以下の様に簡単に書け、しかも読む人にも分かりやすいのです。

func tee(in chan string) (<-chan string<-chan string) {
    out1, out2 := make(chan string), make(chan string)
    go func() {
        defer close(out1)
        defer close(out2)
        for v := range in {
            out1, out2 := out1, out2
            for i := 0; i < 2; i++ {
                select {
                case out1 <- v:
                    out1 = nil
                case out2 <- v:
                    out2 = nil
                }
            }
        }
    }()
    return out1, out2
}
ちなみに tee が何故 tee と呼ばれているかというと、入力と出力の枝が T になっているからです。(豆知識)

本書ではあり得る非同期のパターンの多くを分かりやすく解説してくれています。そして Go 言語では非同期処理をどの様に解決できているかを詳しく紹介してくれています。時折混ぜられるユーモアのある話もなかなか良かったのですが、僕が思うにこの本の良い所は、Go 言語で悩むであろう並行処理の設計方針について敢えて問題を読者に問い掛け、そして具体例で解き明かしつつ説明している所だと思います。そして多くのページを割いて間違った使い方についても紹介されています。どうするとデッドロックが起きてしまうのか、どうするとライブロック(道で向かいから来る人と同じ方向に避けてしまうアレです)が起きてしまうのか、なぜこのコードはスケールしないのか、多くのシーンをコード付きで説明してくれています。

特に興味深かったのが、channel を使うべきか、sync を使うべきかの決定木の図でした。yes/no に従いどちらを使うべきかをアドバイスしてくれます。この記事では紹介しませんが、ぜひ本書を読んで確認して頂くと良いかと思います。

一つ言っておくと、本書を読んでも Go での非同期処理のコードがスラスラ書ける様にはならないと思います。ただ今までなんとなく分かった気分で書いてしまっていた非同期処理が明確な理由を持って理解できる様になると思います。

Go を触り始めの方が多くハマるであろうケースも沢山書かれています。例えば Go の非同期処理に初めて触れた人は、きっと無限に goroutine を作り続けてしまうかもしれません。また Go が少し分かってくると、重たい処理を goroutine で起動できる様になりますが、goroutine とメイン goroutine で同時に変数を触ってしまい不整合を起こしてしまうというケースもあります。さらに Go が分かってきたとしても、使いどころを間違ってしまい goroutine を使ってヌルヌル動く様にはなったけれど結果としてパフォーマンスが大して速くならないといったケースもあるでしょう。Go を使ったとしても非同期処理を書くのは難しいのです。それでも Go が多くの人から評価されているのは少ないイディオムで多くの非同期パターンを記述できるからなのです。

goroutine と channel だけ使えば多くの非同期パターンを記述できます。

Big Sky :: Go 言語の非同期パターン

Go は goroutine という非同期の仕組みを提供していますが、使い方次第では色々なパターンが実装できる為、初めて goroutine を見た人はどの様な物が正解なのか分からない事があります。以...

https://mattn.kaoriya.net/software/lang/go/20180531104907.htm

2種類のイディオムだけでこれだけのパターンを書けるのです。とは言っても goroutine と channel で記述するとタイムアウトやキャンセルと言った処理は冗長になり得ます。Go はこの問題に context パッケージを提供する事で解決しているのですが、本書ではこの context が生まれた経緯や、なぜこの context が goroutine/channel の問題を解決できるのかといった根底の話も書かれています。

非同期処理が苦手な人が読むと少しクラクラするかもしれませんが、全体を通してもとてもリズムが良く、解説の順番も「上手いな」と思える内容でとても良かったです。そして今 Go 言語を書いている皆さんは今後も goroutine と channel を使って多くのコードを書くと思います。そのコードが何故必要になるのか、この非同期処理でどれほど効果が得られるのか、なぜスケールしないのか、そういった事を学びたい人らば必読の本だと思います。

Go言語による並行処理 Go言語による並行処理
Katherine Cox-Buday, 山口 能迪
オライリージャパン 単行本(ソフトカバー) / ¥3,080 (2018年10月26日)
 
発送可能時間:

Posted at by