技術をかじる猫

適当に気になった技術や言語、思ったこと考えた事など。

微分を使ってパラメータを求める(最急降下法or勾配降下法)

お題は、ある不明関数 f にっついて、t という値にもっとも近くパラメータを見つける。
ということで、ちょこちょことやってみた。

計算したい関数はこれで、もっとも 0 に近くなる x を求めたい。
このくらいならふつーに計算すればできそうなものだが、あえてプログラム的に。

def func(x: float) -> float:
    """計算対象の関数

    Arguments:
        x {float} -- 引数

    Returns:
        float -- 計算結果
    """

    return 2 * (x ** 2) + 2 * x + 1

ある値 x に対する誤差の計算を以下の様にかける。

def loss(f: func, x: float, t: float) -> float:
    """損失関数

    Arguments:
        f {func} -- 値を求める関数
        x {float} -- 確認する x 値
        t {float} -- 正しい値

    Returns:
        float -- 誤差値
    """
    y = f(x)
    return (t - y)**2

この時、関数は前述の func で、求めたい t は 0.0 固定でいいので

loss_func = lambda x: loss(func, x, 0.0)

こんな感じにしておく。
ここで、この勾配を求めるのに、微分をしてみる。

微分は、グラフ上の接線、ないしは接点であるとも言え、その定義は極限で 0 に収束させる時の値だったはず。
なら処理的にはこんな風に書けるはず。

def numerical_gradient(f: func, x_value: float):
    h = 1e-4
    grad = 0.0

    x1 = x_value + h
    x2 = x_value - h

    fhx1 = f(x1)
    fhx2 = f(x2)

    grad = (fhx1 - fhx2) / 2*h
    return grad

その上で、800 回ほど誤差計算とパラメータの移動をさせてみた

loop = 800
lern = 0.001
current_x = 3.5  # 開始値
log = []


for n in range(loop):
    # 微分して
    gradient = numerical_gradient(loss_func, current_x)
    # 誤差を元にあるべき x に近づける
    current_x += gradient * lern
    log.append(current_x)

学習係数をかなり小さくしてやらないと、プラスとマイナスでやたらと振れた挙句オーバーフローを起こすので注意

f:id:white-azalea:20190206172104p:plain

プラスからでもマイナスからでも、最終的に x=-0.5 に収束する。
ニューラルネットワークの学習計算から論理を切り出してみたのだが、なんとなく良さそう。

ゼロDeep4章のニューラルネットワーク学習

論理としては理解できたのでメモ。

  1. 教師データを食わせたとき、その演算結果(行列、インデックスがそれぞれ分類を示す)と、出てきてほしい結果(これまた行列)の誤差を行列で取得する関数を用意する。(損失関数)
  2. この損失関数と、現在のニューラルネットワークの重み、バイアスをパラメータに微分して、パラメータのどの方向に変動させるべきかを見る。 最急降下法というようだ。
  3. 出てきた誤差を、学習係数を踏まえて実際にパラメータへ入力する。

という流れで、ニューラルネットワークの各階層をの「重み」「バイアス」を弄っていく。
というのがどうも学習の論理らしい。

そこまでは腑に落ちた。
で、実際やってみたところ、サンプルの時点で処理オワンネ(汗

StudyDocs/two_layoput_net.py at master · Sunao-Yoshii/StudyDocs · GitHub

それも当然で、サンプルだけで見ると、ニューロンの数が

  • 入力層: 784
  • 出力層: 100

このニューロン1個あたりで「重みとバイアスで2回」x ニューラルネット判定(入力層と、出力層で2回) x 微分(前後計算で 2 回)で 8 回計算する羽目に…。
しかも行列計算、浮動小数店計算。

ベンチマークもびっくりの計算量ですよ。
しかも論理を説明するため、処理時間を考慮しないシングルスレッド…そりゃ無理でんがな

Checkstyle の Metrics 近辺の内容を日本語で分かり易く

checkstyle.sourceforge.net

個人的にチェックスタイルでもっとも素晴らしいチェックの項目群だと思うのだけど、これで引っかかると、意味が理解できなくて直すの止める人とか、「チェックが開発の邪魔だ」とか言ってくる人が多いので、なるだけ分かり易くメモしておく。

BooleanExpressionComplexity

要するに && とか || を繋げすぎた条件式なんてバグるんだから書くんじゃないというチェック。
この数値は、条件結合数( ||&& の数 )で単純増加する。

個人的にここの 3 位が好み。
人間の脳みそは 同時に 5 個位しか処理できない そうですよ。

パラメータ

名称 意味 初期値
max 最大で許容する複雑度の数字 3
tokens チェックする演算子 && & || | ^

ClassDataAbstractionCoupling

中で使用(new とか)しているクラス数チェック。

何より、論理クラスをたくさん import しておいて、見通しが効くのかって問題がある。
これで何がわかるかというと、クラス保守のしやすさ(当たり前だけど、たくさん依存してるクラスなんてテスト書けないぞ)。
もっというと、数字が大きい=そのクラス色々やりすぎてないか?設計粒度おかしくない?って話

パラメータ

名称 意味 初期値
max 最大で許容するクラス数 7
excludedClasses チェック対象外クラス(こいつらは論理ロジックではないので、無視する) HashMap, ArrayList, String, float, TreeSet, List, Boolean, Void, Override, Short, IllegalArgumentException, UnsupportedOperationException, HashSet, void, Character, IndexOutOfBoundsException, byte, double, Double, LinkedList, Float, Byte, SortedMap, Long, Throwable, Object, Class, Map, IllegalStateException, Set, StringBuilder, SuppressWarnings, SortedSet, long, RuntimeException, Deprecated, NullPointerException, Queue, SecurityException, FunctionalInterface, TreeMap, Deque, int, Exception, Integer, SafeVarargs, StringBuffer, boolean, char, short, ArrayIndexOutOfBoundsException
excludeClassesRegexps チェック対象外クラスの正規表現。Voクラスなんかの設定をしておくと吉 ^$
excludedPackages チェック対象外パッケージ。ユーティリティパッケージでも指定しておくと吉 {}

ClassFanOutComplexity

参照しているクラス数。

ClassDataAbstractionCoupling に似ているが、こっちはファイル単位で参照してるクラス数のカウント。
あくまで参照のカウントなので、中でインスタンス化してるかは問題ではなくて、メソッドの返り値を使ったとかそういうレベルでもカウントされる。

ClassDataAbstractionCoupling に比べると数字が上がり易くなる傾向はあるが、この値が大きいということは、把握すべきクラス数が多いことを意味してる。
あんまし大きいと、テストコードが書きにくくなる。

個人的にもこれはデフォルトくらいでよい

パラメータ

名称 意味 初期値
max 最大で許容するクラス数 20
excludedClasses チェック対象外クラス(こいつらは論理ロジックではないので、無視する) HashMap, ArrayList, String, float, TreeSet, List, Boolean, Void, Override, Short, IllegalArgumentException, UnsupportedOperationException, HashSet, void, Character, IndexOutOfBoundsException, byte, double, Double, LinkedList, Float, Byte, SortedMap, Long, Throwable, Object, Class, Map, IllegalStateException, Set, StringBuilder, SuppressWarnings, SortedSet, long, RuntimeException, Deprecated, NullPointerException, Queue, SecurityException, FunctionalInterface, TreeMap, Deque, int, Exception, Integer, SafeVarargs, StringBuffer, boolean, char, short, ArrayIndexOutOfBoundsException
excludeClassesRegexps チェック対象外クラスの正規表現。Voクラスなんかの設定をしておくと吉 ^$
excludedPackages チェック対象外パッケージ。ユーティリティパッケージでも指定しておくと吉 {}

CyclomaticComplexity

循環的複雑度とよく言われる。

10 超えたら、汚すぎて読めないっていって、レビュー突っ返していいレベル(だと個人的に思ってる)。
関数型プログラミングやってる連中だと、これで 5 行くとか、よほどの理由があるかさもなくばバカかと思われる。

if とか while とかを使うことで発生する、処理分岐数の概算値。
具体的には boolean 式 1 個単位でカウントしてる

公式曰く「テストコードを書く前提なら、1-4 に抑えたい。5-7 ならかろうじてテストできるんじゃない?8-10 も行ったらリファクタを検討すべき。」

著作権法はプログラムにするとバグりやすい って話の中で、ドワンゴ CTO の人が、以下のように言及してる。

循環的複雑度が75を超えるとバグの混入確率は98%、「いかなる変更も誤修正を生む」状態になる

個人的見解なら 5 超えたらテストコード書きたくない時点でクソコードだと断じたい。

パラメータ

名称 意味 初期値
max 最大許容する分岐数 10
switchBlockAsSingleDecisionPoint switch 分岐はcase含めて 1 とカウントするか?(false では case ごとにカウント) false
tokens チェック項目 while do for if switch case catch ?(三項演算子) && ||

JavaNCSS

コメント化されてないソースのステートメント(処理)数カウンタ。

公式曰く:

理論的根拠:大きすぎるメソッドやクラスは読みにくく、維持するのに費用がかかります。 NCSSの数値が大きいということは、メソッドやクラスが、より小さな単位に分解されるべき責任や機能を多すぎることを意味します。

個人的には methodMaximum = 20, classMaximum = 500 位の設定がテストしやすいのでオススメ。
関数型言語やってる人なら多分この位でいけるはず。

パラメータ

名称 意味 初期値
methodMaximum 1メソッドあたりの最大ステートメント 50
classMaximum 1クラスあたりの最大ステートメント 1500
fileMaximum 1 ファイルあたりのステートメント 2000

NPathComplexity

Cyclomatic complexity(循環的複雑度)に近しい計算。
ただし、こちらの数字はネストすると単純カウントではなくて、条件分岐なら条件式の複雑性、三項演算は +2 補正とか、もう少し厳密に計算されれる。

1988年のAT&Tベル研究所でのいくつかの情報源の経験的な数ベース

ただ、「やりすぎてロジックの明瞭性を損なうなよ」とは checkstyle 公式の言。
個人的には 100 は超えたくない。

名称 意味 初期値
max NPathの最大許容値 200

ということで、主要なものの説明。

今日やった事メモ

何がしたいのかというと、log4javascript を試したかっただけ。
どんなリクエストが飛ぶかもよくわからなかったので、PHP でざっくりキャプチャするかーって事で、まずはPHP用意。

qiita.com

Docker のおかげで世の中楽になりました。

まずはダウンロード

log4javascript.org

ってもわかりやくすダウンロードリンクがあるのでホイ。

簡単に使ってみる

ダウントードして、以下のような配置をしてみた。

  • log4javascript.js
  • index.html
  • example.js

index は特に何もしてなくて、二つの javascript を取り込むだけ。

<script type="text/javascript" src="/log4javascript.js"></script>
<script type="text/javascript" src="/example.js"></script>

そして、example.js は

var log = log4javascript.getDefaultLogger();
log.info("Example info");

アクセスしてみると…なかなかリッチな何かが出てきたw

f:id:white-azalea:20190127212701p:plain
log4js

ドキュメントのチュートリアルを見てもわかるが、trace, debug, info, warn, error があるらしい。
しっかし Javascript コンソールに出るだけかと思ったよ…

エラーコードも

try {
  throw new Error("Faking something going wrong!");
} catch (e) {
  log.error("An error occurred", e);
}

この通り

f:id:white-azalea:20190127213135p:plain

AjaxAppender

なんでも、ログ情報を非同期にサーバに送るアペンダがあるので、早速試してみた。

var log = log4javascript.getDefaultLogger();
var ajaxAppender = new log4javascript.AjaxAppender('http://localhost/log.php');

log.addAppender(ajaxAppender);
log.info("Example info");

すると、chrome には以下の様な通信が発生していた。

f:id:white-azalea:20190127213757p:plain

f:id:white-azalea:20190127213920p:plain

なるほど、普通にやると HTTP POST でフォームパラメータで送信されるようだ。
因みに、ログのサブウインドウはやはり表示された。

このウィンドウが邪魔な時は、デフォルトのアペンダを丸ごとクリアしてしまえば良さそう。

var log = log4javascript.getDefaultLogger();
var ajaxAppender = new log4javascript.AjaxAppender('http://localhost/log.php');

// remove default appender
log.removeAllAppenders();
// set ajaxAppender only
log.addAppender(ajaxAppender);
log.info("Example info");

尚、アペンダの設定を見ればわかるのだけど、log4j で properties とかに書いてたものは軒並みメソッドとして存在しているらしい。
なので、共通的な設定をしてアプリケーション全体に噛ませるなら、ロガージェネレータ関数でも用意するしかなさげ。

log4javascript 1.4 manual

デフォルトでは、非同期にポンポンログを投げる設定だけど、通常時はログを投げずに unload イベント発生時にまとめて投げる設定なんかもあったので覚えておくと良さそう。
なんにしても送信設定周りはコイツのお仕事。

続きを読む

単体テストコードの書きやすい設計入門

というようなドキュメントを github で書いたので、メモ

StudyDocs/RefactorForUnitTest.md at feature/RefactorExample · Sunao-Yoshii/StudyDocs · GitHub

結論だけ書くと

  • オブジェクト依存は、置き換え可能なようにしよう。
  • オブジェクト変数は極力書き換えない、書き換えるならそうした処理は局所化する。
  • メソッドの機能は小さく単純なほどテストしやすい。

テストコードの工数見積もりを大きく見積もる人はこれを意識してみよう。
設計する権限が無い人にはスマン、それは無理だ(汗

ちょっとだけ書いたメモ(2)

昨日の続き。

テーブルの形を知らずに Insert だけぶち込むのは、INSERT 文をむりくり生成してやればできなく無いのだけど、その場合文字列とか日付型とか、利用者側が型を意識しなくてはならず、扱いがめんどい。
というか TSV 作るときは、値だけあってればいちいちダブルクォートとか色々やりたくないと思うのだ。

なら、INSERT を行う前に、動的にスキーマをぶっこ抜く必要があって、まだ実行してないけどこんな感じでいけそう?(間違ってたらそのうち直す)

// connection は java.sql.Connection
DatabaseMetaData metaData = connection.getMetaData();
try (ResultSet rs = metaData.getColumns(null, schemaName, tableName, "%")) {
    while (rs.next()) {
        rs.getString("COLUMN_NAME");
        rs.getInt("TYPE_NAME");
        rs.getInt("COLUMN_SIZE");
    }
}

と言うような事が以下から読めた。

docs.oracle.com

ちなみに、TYPE_NAME で拾ってこれるのが整数なのだけど、java.sql.Types 内に整数定義が存在してるので、switch なり == なりで型を拾えるらしい。
これで型を拾って型変換コンバータに食わせればいいっぽいかな?

よく Java の O/R マッパー関連が、テーブル定義を引っこ抜いてる臭い挙動をしてたので、あるんだろうなーと思って探したら案の定あった系。
要するに誰かの出来るものは、だいたいの場合、探せば見つかる。

見つからないのは特許系のアルゴリズム(苦笑

ちょっとだけ書いたメモ

TSV をテーブルにぶち込むライブラリがあったらテスト捗るかなーと思って少しづつカキカキ。
UT コミでおよそ 1h

目的がテスト用途でしかないので、Iterable とか Stream で返すのは諦めた(というかめんどくさかった)。
作りを見えばわかるけど、UTF-8 で書かれたTSVを、1行目をカラム名、2行目以降をデータとして取り込んで Map のリストにしてるだけ。

次はテーブル名指定で、スキーマ解析→スキーマに合わせてカラムデータの型変換→Insert 実行でできるかなーと。
一応 JDBC 経由でテーブルのカラム定義とかぶっこぬけるっぽいので、多分作れる。

ちなみにエスケープとか開業とかは考慮してない。

/**
 * From static TSV (Tab separated value) data to column stream.
 *
 * @author S.Yoshii
 */
public class TsvDataSource {

    /**
     * Path of TSV file.
     */
    private final Path path;

    /**
     * Specified load target.
     * @param dataSource load target TSV path string.
     */
    public TsvDataSource(String dataSource) {
        this(Paths.get(dataSource));
    }

    /**
     * Specified load target.
     * @param path load target path.
     */
    public TsvDataSource(Path path) {
        this.path = path;
    }

    public List<Map<String, String>> loadDataSource() throws IOException {
        List<Map<String, String>> lists = new LinkedList<>();
        List<String[]> rows;

        try(BufferedReader reader = Files.newBufferedReader(this.path, Charset.forName("UTF-8"))) {
            Stream<String> lines = reader.lines();

            rows = lines.filter(v -> v != null && v.length() > 0)
                    .map(str -> str.split("\t"))
                    .filter(v -> v.length > 0)
                    .collect(Collectors.toList());
        }

        // load headers.
        String[] headers = rows.get(0);

        // convert to maps.
        for (String[] strs : rows) {
            if (strs == headers) continue;

            HashMap<String, String> row = new HashMap<>(strs.length);
            for (int n = 0; n < strs.length; n++) {
                row.put(this.toLower(headers[n]), this.wrapAsNull(strs[n]));
            }
            lists.add(row);
        }

        return lists;
    }

    /**
     * return lower case column name is not null or empty.
     * @param header source target string.
     * @return lower case column name.
     * @throws IOException Invalid column name.
     */
    private String toLower(String header) throws IOException {
        String test = this.wrapAsNull(header);
        if (test == null) throw new IOException("Cant set column name as null or empty.");
        return test.toLowerCase();
    }

    /**
     * return null if str is empty;
     * @param str check target str.
     * @return return str if not null or empty.
     */
    private String wrapAsNull(String str) {
        return str == null || str.trim().length() == 0 ? null : str;
    }
}