It’s now or never

IT系の技術ブログです。気になったこと、勉強したことを備忘録的にまとめて行きます。

【Ethereum】Contractをネットワークにデプロイする

Ethereumの勉強を引き続きしています。
Contractをネットワークへデプロイする場合、 今までは、Ethereum WalletRemix経由で行っていたのですが、ソースをコンパイルしていデプロイする方法がよくわからなかったので調べてまとめてみました。

古いバージョンのgethでは、コンソール上でコンパイルができるようなのですが、最新のgethではできないようです。
なので今回は、javascriptAPIであるweb3を使ってSollidityのソースをコンパイルします。

Solidityのインストール

npm install solc

web3のインストール

npm install web3@0.20.0
  • JavaScriptAPI環境であるweb3をインストールします。
  • 現状のnpmの最新バージョンは、ベータ版の1.0.0-beta.13でしたが、想定のAPIが上手く動かなかったりとハマったためStable版の0.20.0をインストールしました。

ノード起動

geth --datadir <ネットワークのrootディレクトリ> --mine --nodiscover --maxpeers 0 --networkid <ネットワークID> --rpc --rpcport 8545 --rpcaddr "0.0.0.0" --rpcapi="db,eth,net,web3,personal,web3" --rpccorsdomain "*" 
  • プライベートネットワークのノードを起動します。
  • datadirはプライベートネットワークを作成したディレクトリです。
  • networkidは任意のIDで大丈夫です。
  • web3はRPC経由でアクセスするため、RPCを許可します。
  • プライベートネットワークの構築方法は以下を参照いただければ幸いです。

inon29.hateblo.jp

Contract

pragma solidity ^0.4.0;

contract TestContract {
    uint data;

    function set(uint x) {
        data = x;
    }

    function get() constant returns (uint) {
        return data;
    }
}
  • 今回は、このテスト用のContractをコンパイルしてノード上にデプロイしてみます。
  • Javascriptを実行するディレクトリ上に上記ファイルを配置しておきます。

Javascript

// 必要なパッケージをインポート
const fs = require("fs");
const solc = require('solc');
const Web3 = require('web3');

// web3の初期化
web3 = new Web3();
// プライベートネットワークと接続
if (!web3.currentProvider) {
    web3.setProvider(new web3.providers.HttpProvider("http://localhost:8545"));
}
// contractソースをファイルから読み込み
// ここは、ソースを文字列として読み込んでいるだけです。
let source = fs.readFileSync('test_contract.sol', 'utf8');
// コンパイル
let compiledContract = solc.compile(source, 1);
// ABIを取得(配列のキーはなぜか頭に:が必要でした)
let abi = compiledContract.contracts[':TestContract'].interface;
// コントラクトのバイトコードの取得(頭に0xを付けないと後述のAPIで弾かれる)
let bytecode = "0x" + compiledContract.contracts[':TestContract'].bytecode;
// デプロイに必要なGasを問い合わせる
let gasEstimate = web3.eth.estimateGas({data: bytecode});
// コントラクトオブジェクトの生成
let TestContract = web3.eth.contract(JSON.parse(abi));
// デプロイアカウントのLockを外す
web3.personal.unlockAccount(web3.eth.accounts[0], "<アカウントのパス>");
// ネットワークにデプロイ
// コンストラクタがある場合は、ハッシュの前の引数にコンストラクタのパラメタを渡すことで初期化可能
TestContract.new({from: web3.eth.accounts[0], data:bytecode, gas:gasEstimate });
node deploy.js

デプロイの確認

INFO [08-02|22:49:38] Submitted contract creation              fullhash=0x7605a5560f0d729f89f5b1e4ea632d84b4ebd038f930d53620c521adc8f90f0d contract=0xd06c3fce760aa1ab550b0cde8fe4792a1e4a3e11
  • 上記のようなログがgethに出力されていればデプロイに成功しています。

デプロイしたコントラクトを使う場合

const Web3 = require('web3');

web3 = new Web3();
// providerとの接続
if (!web3.currentProvider) {
    web3.setProvider(new web3.providers.HttpProvider("http://localhost:8545"));
}
 
let abi = [{"constant":false,"inputs":[{"name":"x","type":"uint256"}],"name":"set","outputs":[],"payable":false,"type":"function"},{"constant":true,"inputs":[],"name":"get","outputs":[{"name":"","type":"uint256"}],"payable":false,"type":"function"}];
let address = "0xd06c3fce760aa1ab550b0cde8fe4792a1e4a3e11";
let TestContract = web3.eth.contract(abi);
let contract = TestContract.at(address);
let num = contract.get.call();

console.log(num.toPrecision());
  • 一度ネットワークにデプロイされたコントラクトはABIコントラクトのアドレスからアクセスすることが可能です
0
  • 現状は、値のセットがされていないため0が返ります。

参考リンク

https://github.com/ethereum/wiki/wiki/JavaScript-API#web3ethcontract

【Ethereum】Ethereumで独自のトークンの作成してみる

Ethereumのトークンとは

Ethereum上でトークンとよばれるコントラクトを作成することで独自の暗号通貨を作成することが可能です。 トークンを使うことで、コイン、ポイントなど様々な仮想的な価値をEthereumの仕組みの上で利用することができます。 トークンはEhereumのネットワーク上のアカウント間で自由にやりとりすることが可能です。

トークンコンストラクト

トークンを定義するためのコンストラクトには、幾つかの標準APIが定義されています。
トークAPIの議論についてはここでされています。
Ethereumのトークンのサンプル実装は、ここにあります。

pragma solidity ^0.4.8;
contract tokenRecipient { function receiveApproval(address _from, uint256 _value, address _token, bytes _extraData); }

contract MyToken {
    /* Public variables of the token */
    string public standard = 'Token 0.1';
    string public name;
    string public symbol;
    uint8 public decimals;
    uint256 public totalSupply;

    /* This creates an array with all balances */
    mapping (address => uint256) public balanceOf;
    mapping (address => mapping (address => uint256)) public allowance;

    /* This generates a public event on the blockchain that will notify clients */
    event Transfer(address indexed from, address indexed to, uint256 value);

    /* This notifies clients about the amount burnt */
    event Burn(address indexed from, uint256 value);

    /* Initializes contract with initial supply tokens to the creator of the contract */
    function MyToken(
        uint256 initialSupply,
        string tokenName,
        uint8 decimalUnits,
        string tokenSymbol
        ) {
        balanceOf[msg.sender] = initialSupply;              // Give the creator all initial tokens
        totalSupply = initialSupply;                        // Update total supply
        name = tokenName;                                   // Set the name for display purposes
        symbol = tokenSymbol;                               // Set the symbol for display purposes
        decimals = decimalUnits;                            // Amount of decimals for display purposes
    }

    /* Send coins */
    function transfer(address _to, uint256 _value) {
        if (_to == 0x0) revert();                               // Prevent transfer to 0x0 address. Use burn() instead
        if (balanceOf[msg.sender] < _value) revert();           // Check if the sender has enough
        if (balanceOf[_to] + _value < balanceOf[_to]) revert(); // Check for overflows
        balanceOf[msg.sender] -= _value;                     // Subtract from the sender
        balanceOf[_to] += _value;                            // Add the same to the recipient
        Transfer(msg.sender, _to, _value);                   // Notify anyone listening that this transfer took place
    }

    /* Allow another contract to spend some tokens in your behalf */
    function approve(address _spender, uint256 _value)
        returns (bool success) {
        allowance[msg.sender][_spender] = _value;
        return true;
    }

    /* Approve and then communicate the approved contract in a single tx */
    function approveAndCall(address _spender, uint256 _value, bytes _extraData)
        returns (bool success) {
        tokenRecipient spender = tokenRecipient(_spender);
        if (approve(_spender, _value)) {
            spender.receiveApproval(msg.sender, _value, this, _extraData);
            return true;
        }
    }        

    /* A contract attempts to get the coins */
    function transferFrom(address _from, address _to, uint256 _value) returns (bool success) {
        if (_to == 0x0) revert();                                // Prevent transfer to 0x0 address. Use burn() instead
        if (balanceOf[_from] < _value) revert();                 // Check if the sender has enough
        if (balanceOf[_to] + _value < balanceOf[_to]) revert();  // Check for overflows
        if (_value > allowance[_from][msg.sender]) revert();     // Check allowance
        balanceOf[_from] -= _value;                           // Subtract from the sender
        balanceOf[_to] += _value;                             // Add the same to the recipient
        allowance[_from][msg.sender] -= _value;
        Transfer(_from, _to, _value);
        return true;
    }

    function burn(uint256 _value) returns (bool success) {
        if (balanceOf[msg.sender] < _value) revert();            // Check if the sender has enough
        balanceOf[msg.sender] -= _value;                      // Subtract from the sender
        totalSupply -= _value;                                // Updates totalSupply
        Burn(msg.sender, _value);
        return true;
    }

    function burnFrom(address _from, uint256 _value) returns (bool success) {
        if (balanceOf[_from] < _value) revert();                // Check if the sender has enough
        if (_value > allowance[_from][msg.sender]) revert();    // Check allowance
        balanceOf[_from] -= _value;                          // Subtract from the sender
        totalSupply -= _value;                               // Updates totalSupply
        Burn(_from, _value);
        return true;
    }
}
  • 上記がサンプル実装のトークンコントラクトのソースです。
  • プログラム処理としてはそれほど難しくないため、何となく書いてあることは理解できます。

各構文の理解もしていきたいのですが、今回はこのトークンコントラクトのサンプルソースを使って、新しいトークンをデプロイしてみます。

実行環境

  • Ubuntu14.04 on docker

Ethereumのプライベートネットワークを作成する

  • トークンは本番ネットワークではなくプライベートネットワークで作成します。
  • プライベートネットワークの作成については以下を参照ください。

inon29.hateblo.jp

プライベートネットワーク(node)の起動

プライベートネットワークを起動してコントラクトを作成する準備をします。
コントラクトを作成するトランザクションでマイニングが必要なため予めマイニングをスタートしておきます。

geth \
--datadir /ethereum/eth_private  \
--mine  \
--nodiscover  \
--maxpeers 0  \
--networkid 13  \
--rpc  \
--rpcport 8545  \
--rpcaddr "0.0.0.0"  \
--rpcapi="db,eth,net,web3,personal,web3" \
--rpccorsdomain "*" 
  • mine: マイニングを許可する。このオプションを指定するとマイニングが始まります。
  • nodiscover: Gethはデフォルトで自動的に(同じネットワークID)のEthereumネットワークのノード(Peer)を探し接続を試みます。プライベート・ネットで未知のノードとの接続を避けるため、このオプションを指定することで自動Peer探索機能を無効にします。
  • maxpeers: ネットワークに接続するノードの最大数(デフォルト25) 0を指定するとネットワークが無効になります。
  • networkid: ネットワークID(デフォルト1) 1を指定するとグローバルのネットワークに接続されます。
    • 2,3,4も予約されているようです。(2=Morden (disused), 3=Ropsten)
  • rpc: HTTP-RPC serverとして動作を許可する
  • rpcport: HTTP-RPCで使用するポート番号
  • rpcaddr: 接続ホスト
  • rpcapi: HTTP-RPCのインタフェースの種類
  • rpccorsdomain クロスオリジンを許可するリスト

後述するEthereum walletJSON-RPC経由で接続するためにRPCの許可をしています。

送信先のテストアカウントを作成

作成したトークンを送信するためのテストアカウントをコンソール上で作成しておきます。

> personal.newAccount('<任意のパスフレーズ>')
"0xe41253c3218ade0650469b87dcb97a3298d3b5d8"
  • コンソールからアカウントを作成します。

Ethereum walletのインストー

  • Ethereum walletとはEthereumの公式のツールです。Ethereum Walletを使うとウォレットの管理をGUIベースで行うことができます。
  • その他にもEthereum walletでは、コントラクトの作成や独自トークンの管理など便利な機能があります。
  • 今回はこのEthereum walletを使って独自のトークンを作成&デプロイします。

f:id:inon29:20170716182236p:plain### インストー

  • ダウンロードページから自分にあった環境のツールをダウンロードします。
    • 僕はDocker環境上でプライベートネットワークを動かしていますが、Ethereum walletはMac上のアプリとしてインストールしました。

Ethereum walletの起動(Mac環境)

% /Applications/Ethereum\ Wallet.app/Contents/MacOS/Ethereum\ Wallet --rpc http://localhost:8545
  • MacアプリのEthereum walletから先程起動したプライベートノードへ接続します
    • 僕は、Geth環境をDocker上に作成しているため、あらかじめDockerで8545ポートをフォワーディングしています。

f:id:inon29:20170716182536p:plain

  • rpcのセキュリティ警告がでますがOKを押下して進みます。
    • (RPCは接続制御ができないため、テスト環境以外では使わない方がいいと思います。)

f:id:inon29:20170716182624p:plain

  • LAUNCH APPLICATIONを押下します

f:id:inon29:20170716182739p:plain

  • ネットワークの接続に成功するとwallet画面が表示されます
  • 先程作成したアカウント(coinbaseと送信用)が2つ表示されていれば正しく接続されています。

カスタムトークンをデプロイする

  • Ethereum wallet上からカスタムのトークンを作成してみます。
  • アプリ上メニューのCONTRACTS > DEPLOY NEW CONTRACT を押下

f:id:inon29:20170716183035p:plain

f:id:inon29:20170716183046p:plain

  • Deploy contract画面を少しスクロールしてSOLIDITY CONTRACT SOURCE CODEの部分にサンプルソースをそのまま添付します

  • しばらくするとコンパイルが完了するので、画面右のSELECT CONTRACT TO DEPLOYからMy Tokenを選択します

f:id:inon29:20170716183239p:plain

  • 作成するトークンの情報を入力します
  • Initial supply: 初期のトークン総量。今回は10000トークン発行します。
  • Token name: トークンの名前です。今回はMyTokenという名前のトークンを発行します。
  • Decimal units: トークンの小数点以下の桁数を入力します。(下2桁であれば2と入力する)。今回は2にします。
    • Initial supplyが10000でDecimal unitsが2の場合、100.00になります。下2桁までいれて合計10000トークンということです。
  • Token symbol: トークンの単位です。今回はmtとしました。(1トークン=1mt)

f:id:inon29:20170716183410p:plain

この辺、ちゃんと理解できていないのですが、SELECT FEEは1トランザクションを処理する時の手数料で、 大きな値を設定するほど、トランザクション処理の時間が短くなるということっぽいです。 今回は、そのまま指定の値で作成します。

全て入力を終えたらDeployボタンを押下します。

f:id:inon29:20170716183536p:plain

  • 作成するContractの情報が表示されるので、作成アカウントのパスワードを入力してSEND TRANSACTIONを押下します。

f:id:inon29:20170716183654p:plain

f:id:inon29:20170716183749p:plain

  • 承認が終わったら、Contractを作成したアカウントを押下してアカウントの詳細画面へ遷移すると自分が作成したトークン情報が確認できます。

トークンを送信してみる。

f:id:inon29:20170716184207p:plain

  • アカウント画面のMyTokenの右にあるSendボタンを押下してみます。

f:id:inon29:20170716184318p:plain

  • トークンの送信画面になるので、相手先のアカウントのアドレスと送信したいトークン数を入力してトークンを送ってみます。
  • 相手のアカウントのアドレスは予めコピーしておきます。

f:id:inon29:20170716184351p:plain

f:id:inon29:20170716184433p:plain

  • 送信相手のアカウント情報を見てみると30mtが送られていることが確認できます。

参考リンク

【Ethereum】Solidityを使ってSmartContractの作成してみる

引き続き、Ethereumを勉強中です。
前回はEthereumのプライベートネットワークを構築する方法について調べました。

inon29.hateblo.jp

今回は、Ethereumのコアな機能であるSmartContractについて実際に動かしてみたいと思います。

実行環境

  • Ubuntu14.04 on docker

スマートコントラクトとは

スマートコントラクトは、Ethereumを使う上で重要な機能の一つです。
スマートコントラクトとはその名のとおり契約を行う機能で、簡単にいうとブロックチェーンを利用して自動で契約の実行を行うことができるというものです。
例えば、初めに契約の一定の条件と契約時の実行内容を定義しておくと、その条件を満たした時に自動で契約内容が処理される。 といったことが実現できます。
イメージとしては、ブロックチェーン上で行われるやり取り(トランザクション)にブログラムソースが付与されていて、任意のタイミングで実行されるという感じです。
Ethereumではこのスマートコントラクトを柔軟に低コストで実現する機能を提供しています。

コントラクトコード

Ethereumには2つのタイプのアカウントが存在します。

一つはEOA(Externally Owned Account)です。 これは我々ユーザー自身が持つアカウントです。
そしてもう一つがContractです。このContractを使ってスマートコントラクトを実現しています。
Contractは、内部に実行コードである「コントラクト・コード」を持っています。
これはプログラム言語のメソッドのようなイメージです。

Solidity

コントラクトコードは、ネットワーク上で「EVM Code(Ethereum Virtual Machine Code)」と呼ばれる、バイトコードの形式で記述されています。

EVN Codeは低水準言語のため人間には読みにくいコードです。
そのためEVN Codeへのコンパイル言語として「Solidity」というJavascriptライクな言語が用意されています。
Solidityはsolcというコンパイラを使用して、EVMへのコンパイルを行います。

Remix(browser-solidity)

  • Solidityをビルドする環境(IDE)としてRemixというツールが提供されています。
  • 単独でsolcをインストールしてコンパイルすることも可能ですが、今回はRemixを使って実装します。
  • geth上でsolcを連携してgethでコンパイルする方法もあったようですが、最新のバージョン(1.6.6)では使えなくなっているようです。

Remix(browser-solidity)をインストー

git clone https://github.com/ethereum/browser-solidity
cd browser-solidity
npm install

アプリケーションの起動

npm start
  • 起動して127.0.0.1:8080にブラウザ上でアクセスするとRemixの画面が確認できます。

f:id:inon29:20170712152955p:plain

  • 上記のように表示されていれば正しく動いています。

ローカルファイルとの同期

  • Remixではローカルファイル(ディレクトリ)と同期を取ることが可能です。

    • (デフォルトの画面上でソースを追加することもできるのですが、そこで作成したファイルがどこのディレクトリに作成されるがよくわかりませんでした。。)
  • ファイルの同期にはremixdというツールを使います。

インストー
npm install -g remixd
同期ディレクトリの指定
remixd -S <同期を取りたいディレクトリ(絶対パス)>
  • remixdでは65520ポートを使用しているため、127.0.0.1:65520へアクセスできるようにしておきます。
ディレクトリ同期

f:id:inon29:20170712153246p:plain

remixのブラウザ画面に戻って左上のメニューから鎖のようなアイコンをクリックしConnectボタンを押下します。

f:id:inon29:20170712153334p:plain

左のファイルエクスプローラに指定したディレクトリとファイルが表示されていれば正しく同期できています。

Contractの登録

contractを動かす環境が整ったので、実際にサンプルソースを動かしてみます。 まずはcontractのサンプルソースをsolidityで記述し、プライベートネットワーク上に登録してみます。

pragma solidity ^0.4.0;
contract sample {
    int num;
    function set_num(int n){
        num = n;
    }
    function get_num() returns(int) {
        return num;
    }
}
  • 上記が動かすサンプルソースです。
  • pragma solidity ^0.4.0;はSolidityのバージョン記述です。
    • 上記ではバージョン0.4.0のコンパイラ以外は動作しないという意味です。
  • contractの後ろに続くのはコントラクト名です。他の言語でいうクラス名みたいなものです。

細かい文法は割愛しますが、これはnumという変数に値をセットするメソッドset_numと値を取得するメソッドget_numという2つのメソッドを定義したcontractになります。
上記のソースをRemix上のファイルに記述します。

ソースが書けたらGethと連携してプライベートネットワークにこのContractを登録します。 まずは、プライベートネットワークへアクセスします。

geth
\ --datadir /ethereum/eth_private  
\--mine  
\--nodiscover  
\--maxpeers 0  
\--networkid 13
\--rpc
\--rpcport 8545
\--rpcaddr "0.0.0.0"
\--rpccorsdomain "*"
\--unlock 0xd8a4f5db00a5f1648ce8731b15ac3a1f72ce106e 
\console

細かいオプションはここでは割愛します。
プライベートネットワークの作成方法については、こちらの記事を参照いただけると幸いです。

inon29.hateblo.jp

  • rpc: JSON-RPCの許可
  • rpcport: JSON-RPCのポート
  • rpcaddr: JSON-RPCの接続ホスト
  • unlock: アカウントへの接続にはアカウントのロックを解除する必要があります。ここでは予めContractを登録するアカウントのロックを解除するためにアドレスを指定しています。

Remixは、nodeとJSON-RPC経由でアクセスするためノードの起動オプションとしてJSON-RPCを許可する必要があります。
環境に合わせて適宜ポートやアドレスを設定します。

f:id:inon29:20170712153925p:plain

Gethの起動が完了したら、Remix上から作成したコントラクトの登録処理を行います。 Remixの右画面 > Contract > EnviromentWeb3 Providerに変更してください。

f:id:inon29:20170712153945p:plain f:id:inon29:20170712153949p:plain

上記のように接続ノードの情報を求められるのでGeth起動時の情報に合わせて接続します。
エラーが表示されなければ正しく接続できていると思います。
(右画面のAccountに接続ノードのアカウントが反映されていれば、正しく接続できていると思います。)

f:id:inon29:20170712154905p:plain

次に右画面の下部にあるCreateというボタンを押下してContractを登録します。

f:id:inon29:20170712154322p:plain

「Waiting for transaction to be mined…」と表示されています。

INFO [07-12|04:11:21] Submitted contract creation              fullhash=0x972db68b4a28be14baebfd17b466817a4bd2e77f0e0e3dbd5c8c87a324bc6857 contract=0xda1dd064dd2c5cb2aa7f3b4603ee973021edb329

Gethに戻ってみると、上記のようにcontractがSubmitされたログが表示されています。

> eth.pendingTransactions
[{
    blockHash: null,
    blockNumber: null,
    from: "0xd8a4f5db00a5f1648ce8731b15ac3a1f72ce106e",
    gas: 107586,
    gasPrice: 18000000000,
    hash: "0x972db68b4a28be14baebfd17b466817a4bd2e77f0e0e3dbd5c8c87a324bc6857",
    input: "0x6060604052341561000f57600080fd5b5b60ce8061001e6000396000f30060606040526000357c0100000000000000000000000000000000000000000000000000000000900463ffffffff1680633e27a8e8146047578063545a48d014606d575b600080fd5b3415605157600080fd5b6057608d565b6040518082815260200191505060405180910390f35b3415607757600080fd5b608b60048080359060200190919050506097565b005b6000805490505b90565b806000819055505b505600a165627a7a723058204eddaa62d484bcd6eb395ac4fdccd0dcf6c06e86a755c47db93b7cab60d930500029",
    nonce: 8,
    r: "0x76e10da1414ee85ea215b78d9b3e97e5021aeb4d47617c05395cc10195042d8c",
    s: "0x5b3a86db12a335952bb99ee17ff6bd0939204cb43ad56fec5b994c9b830c313a",
    to: null,
    transactionIndex: 0,
    v: "0x3d",
    value: 0
}]

今の状態は、マイニングがされていないためノードに登録されたContractがブロックには登録されていない状態です。
(上記のようにpendingTransactionsコマンドで登録されていないトランザクション情報が確認できます。)
これをGeth上でマイニングしてブロックに登録します。

> miner.start()
INFO [07-12|04:16:43] Updated mining threads                   threads=0
INFO [07-12|04:16:43] Transaction pool price threshold updated price=18000000000
INFO [07-12|04:16:43] Starting mining operation
null
> INFO [07-12|04:16:43] Commit new mining work                   number=2470 txs=1 uncles=0 elapsed=600.469µs
INFO [07-12|04:16:53] Successfully sealed new block            number=2470 hash=ddb1b6…f5f1ef
INFO [07-12|04:16:53] 🔗 block reached canonical chain          number=2465 hash=2ae180…48ff9c
INFO [07-12|04:16:53] 🔨 mined potential block                  number=2470 hash=ddb1b6…f5f1ef
INFO [07-12|04:16:53] Commit new mining work                   number=2471 txs=0 uncles=0 elapsed=586.566µs

マイニングをしばらくして、再度eth.pendingTransactionsを実行するとペンディングトランザクションがなくなっていることがわかります。

f:id:inon29:20170712154453p:plain

Remixに戻ってみると止まっていた処理が完了していることがわかります。 これでContractをアカウントに登録することができました。

Contractの実行

次は登録したContractを実際に動かしてみます。

Contractに作成ユーザー以外がアクセスするためには、以下の2種類の情報を他のユーザーに伝える必要があります。

  • Contractのアドレス
  • ContractのABI (Application Binary Interface)
    • ※ABIとはパラメタや戻り値の型など関数の定義が記述された情報です

コントラクトのオブジェクトは、Geth上で以下のように生成することができます。

eth.contract(<ABI_DEF>).at(<ADDRESS>);

それでは、この2つの情報をRemixから取得して、ContractObjectを作成してみます。

f:id:inon29:20170712154549p:plain

Remix上で上記ボタンを押下するとAddressがクリップボードにコピーされます。

f:id:inon29:20170712154633p:plain

  • Contract details (bytecode, interface etc)のリンクをクリックするとContractの詳細情報を確認できます。
  • ここにあるInterfaceという項目がABIになります。

この2つを使ってcontractのオブジェクトを作成してみます。

> var address = "0xed9d193ecdd86033dbfd74088f3cbfa0f3bb5f55"
undefined
> var abi = [{"constant":false,"inputs":[],"name":"get_num","outputs":[{"name":"","type":"int256"}],"payable":false,"type":"function"},{"constant":false,"inputs":[{"name":"n","type":"int256"}],"name":"set_num","outputs":[],"payable":false,"type":"function"}]
undefined
> var contract = eth.contract(abi).at(address);
undefined
> contract
{
  abi: [{
      constant: false,
      inputs: [],
      name: "get_num",
      outputs: [{...}],
      payable: false,
      type: "function"
  }, {
      constant: false,
      inputs: [{...}],
      name: "set_num",
      outputs: [],
      payable: false,
      type: "function"
  }],
  address: "0xed9d193ecdd86033dbfd74088f3cbfa0f3bb5f55",
  transactionHash: null,
  allEvents: function(),
  get_num: function(),
  set_num: function()
}
  • address情報とabi情報を保持したオブジェクトが生成されています。
  • 定義したget_numset_numについてもfunctionとして定義されています。

このcontractの関数を実行するには以下のようにします。

> contract.get_num.call()
> 0
> contract.set_num.call()
> []

Contractへ正しくアクセスできていることが確認できます。

contract.set_num(10,{from:eth.accounts[0]})
or
contract.set_num.sendTransaction(10,{from:eth.accounts[0]})
> contract.set_num(11,{from:eth.accounts[0]})
INFO [07-12|05:56:30] Submitted transaction                    fullhash=0x61086bb8f21b6a7bf3735b7f560996680a6021e4bcf9593799f05515e9731667 recipient=0xed9d193ecdd86033dbfd74088f3cbfa0f3bb5f55
"0x61086bb8f21b6a7bf3735b7f560996680a6021e4bcf9593799f05515e9731667"

上記のように新しいトランザクションが作成されます。
この状態では先程と同様ブロックに登録されていないため、値が反映されていません。

> contract.get_num.call()
0

> eth.pendingTransactions
[{
    blockHash: null,
    blockNumber: null,
    from: "0xd8a4f5db00a5f1648ce8731b15ac3a1f72ce106e",
    gas: 90000,
    gasPrice: 18000000000,
    hash: "0x61086bb8f21b6a7bf3735b7f560996680a6021e4bcf9593799f05515e9731667",
    input: "0x545a48d0000000000000000000000000000000000000000000000000000000000000000b",
    nonce: 10,
    r: "0x256450e05ed9a61147ba3a9b8ba6b63ba03fbec73abdb93ffe4a015a17bacd1",
    s: "0x5ef0655ae3c76673cd608d0a71ef8b1a0a6ac9947fb27492782ef38b262dcabc",
    to: "0xed9d193ecdd86033dbfd74088f3cbfa0f3bb5f55",
    transactionIndex: 0,
    v: "0x3d",
    value: 0
}]

先程と同様にマイニングを行いトランザクションをブロックに登録します。

> miner.start()
・・・
> contract.get_num.call()
11

トランザクションがブロックに登録され、メソッドが実行されると値が正しく反映されます。

> eth.getTransaction("0x61086bb8f21b6a7bf3735b7f560996680a6021e4bcf9593799f05515e9731667")
{
  blockHash: "0xd4e2f46e803deb100c3b852c07919ab2533ac1194f1ae3b0cef71f0b9be85a4e",
  blockNumber: 2476,
  from: "0xd8a4f5db00a5f1648ce8731b15ac3a1f72ce106e",
  gas: 90000,
  gasPrice: 18000000000,
  hash: "0x61086bb8f21b6a7bf3735b7f560996680a6021e4bcf9593799f05515e9731667",
  input: "0x545a48d0000000000000000000000000000000000000000000000000000000000000000b",
  nonce: 10,
  r: "0x256450e05ed9a61147ba3a9b8ba6b63ba03fbec73abdb93ffe4a015a17bacd1",
  s: "0x5ef0655ae3c76673cd608d0a71ef8b1a0a6ac9947fb27492782ef38b262dcabc",
  to: "0xed9d193ecdd86033dbfd74088f3cbfa0f3bb5f55",
  transactionIndex: 0,
  v: "0x3d",
  value: 0
}
  • eth.getTransactionトランザクション情報を参照するとどのブロックに登録されたかも確認することができます。
    • eth.getTransactionの引数はcontractのAddressではなくHashなので注意が必要です。

これでSolidityという言語でContractを動かすことができるようになりました。
Contractは基本的にプログラムの実行なのでエンジニア的には理解し易い気がします。
どのようにトランザクションが処理されるかなど詳しく知りたいことはまだありますが少しずつ覚えたいと思います。

参考リンク

Accessing a shared folder in Remix IDE using Remixd — Remix 1 documentation

Installing Solidity — Solidity 0.4.14 documentation

wiki/[Japanese]-Solidity-Tutorial.md at master · ethereum/wiki · GitHub

【Ethereum】EthereumでPrivateNetworkを作ってみる

仕事の関係でブロックチェーンのプラットフォームであるEthereumを調べることになり、
簡単な動作確認を行うためにプライベートのEthereumのネットワークを作成してみました。

  • ※ 調べながらのものなのでところどころ間違っているかもしれません。
  • ブロックチェーン、仮想コイン自体の知識も深くないためそちらも認識に齟齬があるかもしれません。

動作環境

OS: ubuntu:14.04 * ※ 僕の環境はMacOSなのですが、試行錯誤なので環境がよごれないようにDockerでubuntuをインストールして試しています。 * ※ Docker周辺の操作自体は割愛させていただきます。

Ethereumとは

Ethereumは、ブロックチェーンを使ったアプリケーションのプラットフォームです。 (Ethereumブロックチェーンの技術詳細については、ここでは割愛します。) ブロックチェーンを使ったアプリケーションを作成する方法は大きく分けて2つあります。
1つは、「ゼロからブロックチェーンのネットワークを構築する方法」、もう一つは「既存のブロックチェーンの技術基盤を利用して構築する方法」です。
Ethereumは後者にあたるもので、Ethereumを利用することでブロックチェーンを利用したアプリケーションを低コストで作成することができます。

Gethをインストールする

EthereumのP2Pネットワークに参加するためにGethというクライアントツールが提供されています。 (GethはGo言語製のツールです) Gethをインストールすることで、

といった操作を行うことができます。 まずは、このツールをインストールします。

sudo apt-get install software-properties-common
sudo add-apt-repository -y ppa:ethereum/ethereum
sudo add-apt-repository -y ppa:ethereum/ethereum-dev
sudo apt-get update
sudo apt-get install ethereum

ネットワーク用のディレクトリを作る

mkdir eth_private
cd eth_private

※ このテスト実装では、/ethereumというディレクトリをルートディレクトリとして作業しています。

アカウントをつくる

一番初めのアカウントを定義します。

geth --datadir /ethereum/eth_private account new
WARN [07-09|03:57:57] No etherbase set and no accounts found as default
Your new account is locked with a password. Please give a password. Do not forget this password.
Passphrase:
  • No etherbase set and no accounts found as default

    • この警告はなんでしょう?デフォルトのアカウントがないよという意味のようですが、何もない状態でアカウントを作成しているから?一旦無視します。
  • パスワードを聞かれるため入力する

    • ※ 注意 パスワードを忘れるとあとで確認する方法がないため、忘れない方法で保管しておきます。
Address: {4e2e0ef96802a54c8e1d5c937e38290c42ced5f0}
  • 発行されるとアカウントの公開キーが表示されます。(アカウントのアドレスと呼ばれるものです。)
  • アカウントの作成が成功すると作業ディレクトリ配下にkeystoreというディレクトリが作成されています。
  • さらにその配下にはUTC-xxxから始まるjsonファイルが作成されています。
root@03be5503f430:/ethereum/eth_private# ls -la
total 0
drwxr-xr-x 3 root root 102 Jul  9 03:58 ./
drwxr-xr-x 5 root root 170 Jul  9 03:57 ../
drwx------ 3 root root 102 Jul  9  2017 keystore/
root@03be5503f430:/ethereum/eth_private# ls -la keystore/
total 4
drwx------ 3 root root 102 Jul  9 04:04 ./
drwxr-xr-x 3 root root 102 Jul  9 03:58 ../
-rw------- 1 root root 491 Jul  9 03:58 UTC--2017-07-09T03-58-15.183698355Z--4e2e0ef96802a54c8e1d5c937e38290c42ced5f0
{
    "address": "4e2e0ef96802a54c8e1d5c937e38290c42ced5f0",
    "crypto": {
        "cipher": "aes-128-ctr",
        "ciphertext": "ee886a6635bc095337d643de774ec86c38c32016a1f56c5f3d1e9fb092993193",
        "cipherparams": {
            "iv": "caa069b4c621db2ddd3c9aa11acd9de2"
        },
        "kdf": "scrypt",
        "kdfparams": {
            "dklen": 32,
            "n": 262144,
            "p": 1,
            "r": 8,
            "salt": "8453090efc0f98075afbb046521273909a3c26d6244e2453b83ddae7d6d0540c"
        },
        "mac": "85249864f73dd75ed70e9e8e2123aa0d10d2c5f73d7bfde510fa547804d092d3"
    },
    "id": "a9d3805d-0a8a-4ef4-a89a-1c0949737951",
    "version": 3
}
  • addressは先程作成したアカウントの公開キーと一致するためこれがアカウント情報のようです。(詳細はここでは追いません)

GenesisBlockの設定ファイルを作成する

  • 一番はじめのブロックを定義したファイルをGenesisファイルといいます。
  • ブロックチェーンのネットワークを作成するために、このGenesisファイルを初めに作成します。
{
    "config": {
        "chainId": 13,
        "homesteadBlock": 0,
        "eip155Block": 0,
        "eip158Block": 0
    },
    "difficulty": "200000000",
    "gasLimit": "2100000",
    "alloc": {
        "4e2e0ef96802a54c8e1d5c937e38290c42ced5f0": { "balance": "100000000" }
    }
}
  • chainId: 後に指定するネットワークIDと合わせたID
  • homesteadBlock: 0ブロック以降がHomesteadであるという設定
    • homesteadとは、Ethereumのバージョンのことでこのバージョンの前後で処理が異なるみたいです
  • eip155Block: 不明
  • eip158Block: 不明
  • difficulty: マイニングの難易度。低ければ低い程、短時間でマイニングできるようです。
  • gasLimit: トランザクションで指定できる最大gas
  • alloc: 初期Etherの設定。ここで先程作成したアカウントを指定しています。
    • 単位はwei

GenesisBlockを初期化する

geth --datadir /ethereum/eth_private  init /ethereum/eth_private/CustomGenesis.json
  • ここでもネットワークのディレクトリを指定してます。
INFO [07-09|04:15:31] Allocated cache and file handles         database=/ethereum/eth_private/geth/chaindata cache=16 handles=16
INFO [07-09|04:15:31] Writing custom genesis block
INFO [07-09|04:15:31] Successfully wrote genesis state         database=chaindata                            hash=6aa7ce…33ec9a
INFO [07-09|04:15:31] Allocated cache and file handles         database=/ethereum/eth_private/geth/lightchaindata cache=16 handles=16
INFO [07-09|04:15:31] Writing custom genesis block
INFO [07-09|04:15:31] Successfully wrote genesis state         database=lightchaindata                            hash=6aa7ce…33ec9a
  • 上記のように表示されれば正しく初期化がされています。
root@03be5503f430:/ethereum/eth_private# ls -la
total 4
drwxr-xr-x 5 root root 170 Jul  9 04:15 ./
drwxr-xr-x 5 root root 170 Jul  9 03:57 ../
-rw-r--r-- 1 root root 288 Jul  9 04:14 CustomGenesis.json
drwxr-xr-x 4 root root 136 Jul  9 04:15 geth/
drwx------ 3 root root 102 Jul  9 04:04 keystore/
root@03be5503f430:/ethereum/eth_private# ll geth/
total 0
drwxr-xr-x 4 root root 136 Jul  9 04:15 ./
drwxr-xr-x 5 root root 170 Jul  9 04:15 ../
drwxr-xr-x 7 root root 238 Jul  9 04:15 chaindata/
drwxr-xr-x 7 root root 238 Jul  9 04:15 lightchaindata/
  • geth配下にはchaindataというディレクトリがあり、ここにはブロックチェーンの起源を表すleveldbデータベースファイルと、それが採掘されてブロックチェーンに追加された後のブロックが含まれているようです。
  • つまり新しいブロックが作られるとこのDBにデータが追加されていくようです。

Gethを起動(コンソール)

  • Gethを起動しネットワークにattachします。
  • Gethには対話式のコンソールが用意されています。このコンソールを起動して色々確認してみます。
geth --networkid 13 --datadir /ethereum/eth_private console 2>> /ethereum/eth_private/geth_err.log
instance: Geth/v1.6.6-stable-10a45cb5/linux-amd64/go1.8.1
coinbase: 0x4e2e0ef96802a54c8e1d5c937e38290c42ced5f0
at block: 0 (Thu, 01 Jan 1970 00:00:00 UTC)
 datadir: /ethereum/eth_private
 modules: admin:1.0 debug:1.0 eth:1.0 miner:1.0 net:1.0 personal:1.0 rpc:1.0 txpool:1.0 web3:1.0
 
 >
  • networkidには、Genesisファイルで定義したchainIdを指定します。
  • errer出力をlogファイルに書き出しています。

アカウントの確認

> eth.accounts
["0x4e2e0ef96802a54c8e1d5c937e38290c42ced5f0"]
  • 先程作成したアカウントが確認できます。

etherbase(coinbase)の確認

> eth.coinbase
"0x4e2e0ef96802a54c8e1d5c937e38290c42ced5f0"
  • etherbase(coinbase)とは、そのノードで発掘を行った時にその報酬を紐付けるEOAのアドレスを指します。

所持Etherの確認

> eth.getBalance("0x4e2e0ef96802a54c8e1d5c937e38290c42ced5f0")
100000000
  • 引数にアカウントのaddressを指定します。
  • Genesisファイルで初めに指定したEther(単位はwei)を所持していることが確認できます

Gethを起動(JSON-RPC)

  • コンソールを使わずにネットワークにattachします。
geth \
 --datadir /ethereum/eth_private \
 --mine \
 --nodiscover \
 --maxpeers 0 \
 --networkid 13 \
 --rpc \
 --rpccorsdomain "*"
  • mine: マイニングを許可する。このオプションを指定するとマイニングが始まるようです。
  • nodiscover: Gethはデフォルトで自動的に(同じネットワークID)のEthereumネットワークのノード(Peer)を探し接続を試みます。プライベート・ネットで未知のノードとの接続を避けるため、このオプションを指定することで自動Peer探索機能を無効にします。
  • maxpeers ネットワークに接続するノードの最大数(デフォルト25) 0を指定するとネットワークが無効になります。
  • networkid ネットワークID(デフォルト1) 1を指定するとグローバルのネットワークに接続されます。
    • 2,3,4も予約されているようです。(2=Morden (disused), 3=Ropsten)
  • rpc HTTP-RPC serverとして動作を許可する
  • rpccorsdomain クロスオリジンを許可するリスト
root@03be5503f430:/ethereum/eth_private# geth --datadir /ethereum/eth_private  --mine  --nodiscover  --maxpeers 0  --networkid 13  --rpc  --rpccorsdomain "*"
INFO [07-09|06:02:23] Starting peer-to-peer node               instance=Geth/v1.6.6-stable-10a45cb5/linux-amd64/go1.8.1
INFO [07-09|06:02:23] Allocated cache and file handles         database=/ethereum/eth_private/geth/chaindata cache=128 handles=1024
INFO [07-09|06:02:23] Initialised chain configuration          config="{ChainID: 13 Homestead: 0 DAO: <nil> DAOSupport: false EIP150: <nil> EIP155: 0 EIP158: 0 Metropolis: <nil> Engine: unknown}"
INFO [07-09|06:02:23] Disk storage enabled for ethash caches   dir=/ethereum/eth_private/geth/ethash count=3
INFO [07-09|06:02:23] Disk storage enabled for ethash DAGs     dir=/root/.ethash                     count=2
INFO [07-09|06:02:23] Initialising Ethereum protocol           versions="[63 62]" network=13
INFO [07-09|06:02:23] Loaded most recent local header          number=0 hash=6aa7ce…33ec9a td=200000000
INFO [07-09|06:02:23] Loaded most recent local full block      number=0 hash=6aa7ce…33ec9a td=200000000
INFO [07-09|06:02:23] Loaded most recent local fast block      number=0 hash=6aa7ce…33ec9a td=200000000
INFO [07-09|06:02:23] Starting P2P networking
INFO [07-09|06:02:23] RLPx listener up                         self="enode://9df46f04a32634b47ce6fe8203b8077029550890d5f118b6ad88aaee15db9d96ac819f119a3997c5b5c9cecff395b866c7c45c39f830ce3111d63fb7c5853bed@[::]:30303?discport=0"
INFO [07-09|06:02:23] IPC endpoint opened: /ethereum/eth_private/geth.ipc
INFO [07-09|06:02:23] HTTP endpoint opened: http://127.0.0.1:8545
INFO [07-09|06:02:23] Transaction pool price threshold updated price=18000000000
INFO [07-09|06:02:23] Starting mining operation
INFO [07-09|06:02:23] Commit new mining work                   number=1 txs=0 uncles=0 elapsed=178.695µs
INFO [07-09|06:02:28] Generating DAG in progress               epoch=0 percentage=0 elapsed=3.764s
INFO [07-09|06:02:30] Generating DAG in progress               epoch=0 percentage=1 elapsed=6.657s
  • gethを起動するとしばらく Generating DAG in progressというログが流れる。これはDAGという数Gバイトのデータセットを作成しているログのようです。
  • これはEthashというProof of Workを行うための仕組みで使われるもの。詳しくはここを参照。
  • DAGはデフォルトで$(HOME)/.ethash配下に作成されています。
INFO [07-09|07:09:58] Successfully sealed new block            number=1 hash=ef4ec0…62e718
INFO [07-09|07:09:58] 🔨 mined potential block                  number=1 hash=ef4ec0…62e718
INFO [07-09|07:09:58] Commit new mining work                   number=2 txs=0 uncles=0 elapsed=431.601µs
  • しばらくすると(1,2時間くらい)マイニングが始まりブロックが作成されます

JSON-RPCで通信してみる

  • JSON-RPCで対応しているメソッドはここに定義されています。
クライアントのバージョン(web3_clientVersion)
curl -X POST --data '{"jsonrpc":"2.0","method":"web3_clientVersion","params":[],"id":1}' http://localhost:8545
{"jsonrpc":"2.0","id":1,"result":"Geth/v1.6.6-stable-10a45cb5/linux-amd64/go1.8.1"
Etherの確認(eth_getBalance)
curl -X POST --data '{"jsonrpc":"2.0","method":"eth_getBalance","params":["0x4e2e0ef96802a54c8e1d5c937e38290c42ced5f0", "latest"],"id":1}' http://localhost:8545
{"jsonrpc":"2.0","id":1,"result":"0x5f5e100"}
  • resultには16進数の値が入っている。これを10進数に変換すると100000000となるので正しい値が入っていることがわかります。

参考

Private-network How to Build a Private Ethereum Blockchain

Kerasのサンプルソースが何をやっているか読んでみる

前回は、Kerasのインストールを行い機械学習を行うための環境を構築しました。

inon29.hateblo.jp

しかし、今の状態だと何をどうすればどう使えるのかも全く分からないため、とりあえずKerasのサンプルソースを読んで理解を深めていこうと思います。

その前にニューラルネットワークの前提知識が完全にゼロでソースを理解するのはおそらく難しいだろうなと思い、各所で紹介されている以下の書籍を読みました。

ただし、数学や計算処理については、完全に理解するのは大変そうだったため、難しそうな部分はサラッと流しています。

まずは、なんとなく理解したディープラーニングについて自分なりの解釈をまとめます。

www.oreilly.co.jp

※ これは自分の学習用のまとめになります。 ※ 上記のため単語やアルゴリズムの解釈が間違っている。齟齬がある。場合があるかとおもいます。 ※ 理解がない人が初めて触れた時に噛み砕くための解釈として捉えてもられれば幸いです。

MLPとは何なのか?

多重パーセプトロンの略(Multi-Layer Perceptron)。 パーセプトロンとは複数の信号を入力として受取り一つの結果を返す有るゴリスムのこと。 このパーセプトロンの層を複数に重ねたものを多重パーセプトロンという。 この多重パーセプトロンニューラルネットワークと表現する。 このニューラルネットワークを利用した学習をディープラーニングと呼ぶ。

Kerasを使ったディープラーニングの大まかな流れ

  • ① 多重パーセプトロンのための層を構築する

    • このサンプルではSequentialというモデルを使って構築する
  • ② モデルの最適化アルゴリズムなどの設定(compile)

  • ③ モデルに学習データを与えて学習させる(fit)

  • ④ 損失値(どれだけ精度がでているか)を計算する(evaluate)

※ ② ~ ④を繰り返し、モデルを最適化(損失値を少なく)する

  • ⑤ 実際の予測値を算出する(predict)

大枠こんな感じかと思います。

訓練データとテストデータ

機械学習では、学習推論という2つのプロセスがあります。 学習のプロセスでは予め用意したデータを使ってパラメタをモデルに学習させます。

その時に使うデータとして訓練データというものを用意します。

学習が完了したあとは、モデルに対して推論(実際にある値に対しての分類を抽出する)を行い精度を検証していきます。 上記検証に使うデータをテストデータといいます。

サンプルの読み込み

では、サンプルソースを追っていきたいと思います。

サンプルソースには、reuters_mlp.pyというロイターのニュース・トピック分類のサンプルを使用しています。

このサンプルを選んだ理由としては、自前のMacでは計算にGPUを使えないこともあり、動かして色々と見て見るにはテキストデータの方が計算が早そうだなと思ったためです。

サンプルデータの説明は、こちらのページに載っています。

'''Trains and evaluate a simple MLP
on the Reuters newswire topic classification task.
'''
from __future__ import print_function

import numpy as np
import keras
from keras.datasets import reuters
from keras.models import Sequential
from keras.layers import Dense, Dropout, Activation
from keras.preprocessing.text import Tokenizer

max_words = 1000
batch_size = 32
epochs = 5

print('Loading data...')
# ① サンプルデータの読み込み
(x_train, y_train), (x_test, y_test) = reuters.load_data(num_words=max_words, test_split=0.2)
print(len(x_train), 'train sequences')
print(len(x_test), 'test sequences')

# ② ラベルの最大値を求める
num_classes = np.max(y_train) + 1
print(num_classes, 'classes')

# ③ テキストをベクトル化する
print('Vectorizing sequence data...')
tokenizer = Tokenizer(num_words=max_words)
x_train = tokenizer.sequences_to_matrix(x_train, mode='binary')
x_test = tokenizer.sequences_to_matrix(x_test, mode='binary')
print('x_train shape:', x_train.shape)
print('x_test shape:', x_test.shape)

# ④ ラベルをバイナリクラス行列に変換する
print('Convert class vector to binary class matrix '
      '(for use with categorical_crossentropy)')
y_train = keras.utils.to_categorical(y_train, num_classes)
y_test = keras.utils.to_categorical(y_test, num_classes)
print('y_train shape:', y_train.shape)
print('y_test shape:', y_test.shape)

# ⑤ モデルを構築する
print('Building model...')
model = Sequential()
model.add(Dense(512, input_shape=(max_words,)))
model.add(Activation('relu'))
model.add(Dropout(0.5))
model.add(Dense(num_classes))
model.add(Activation('softmax'))

# ⑥ 学習の設定
model.compile(loss='categorical_crossentropy',
              optimizer='adam',
              metrics=['accuracy'])

# ⑦ 学習の実行
history = model.fit(x_train, y_train,
                    batch_size=batch_size,
                    epochs=epochs,
                    verbose=1,
                    validation_split=0.1)
                    
# ⑧ 損失値の計算
score = model.evaluate(x_test, y_test,
                       batch_size=batch_size, verbose=1)
print('Test score:', score[0])
print('Test accuracy:', score[1])

① サンプルデータの読み込み

(x_train, y_train), (x_test, y_test) = reuters.load_data(num_words=max_words, test_split=0.2)
  • 訓練データとテストデータをKerasが用意しているデータ・セットから読み込んでいます
  • num_wordsで指定しているのは単語の最大数です。このサンプルでは1000個の単語を使用するということになります。
  • test_splitでは、使用するデータに中でテストデータとして使う割合です。(20%をテストデータとして使用するということ)
  • (x_train, y_train), (x_test, y_test)はnumpy.arrayのタプルです
  • trainには訓練データ、testにはテストデータが返ってきます
  • x_train, x_testには、ニュース文字列のインデックス値(文字列を一意に特定する値)が入っています。
    • このサンプルだと8982の文字列が格納されています。((*, 8982)の2次元配列ということ)
    • num_wordsで単語の最大数をしているため各配列に格納されている値は、1~1000の値になります。
    • [1, 2, 699,・・・ 12]のようなデータが格納されている
  • y_train, y_testには、分類のラベルが格納されています。ラベルとは分類される値(正解のようなもの)が格納されています。
    • ラベルはIntegerの値で格納されています

② ラベルの最大値を求める

num_classes = np.max(y_train) + 1
print(num_classes, 'classes')
  • ラベルの値で使われている最大値を求めます。
  • 後述しますが、このラベルの値はKerasの計算においては、0と1の配列として変換する必要がありその要素数として使われます。
    • 1を加算するのは、配列の要素は0から始まるため

③ テキストをベクトル化する

print('Vectorizing sequence data...')
tokenizer = Tokenizer(num_words=max_words)
x_train = tokenizer.sequences_to_matrix(x_train, mode='binary')
x_test = tokenizer.sequences_to_matrix(x_test, mode='binary')
print('x_train shape:', x_train.shape)
print('x_test shape:', x_test.shape)
  • Tokenizerというのは、テキストをベクトル化するクラスです。
  • ①で取得した単語データの配列(長さ8982の配列)は可変長のためこれを1次元の配列に変換します。
    • 例えば、3という数値については、[0, 0, 0, 1]という配列に変換します。
    • [1, 2]という配列であれば[0, 0, 1, 1]となります
  • Tokenizerの初期化時にnum_wordsというパラメタを渡しています。これはnum_words長の配列に変換するということです。
    • 配列にはmax_wordsの数値しか格納されていないため、最大長をmax_wordsの配列にすれば、全てのデータが1次元配列に変換できるということです。
x_train shape: (8982, 1000)
x_test shape: (2246, 1000)
  • printの出力は上記になります。最大長が1000(max_words)の配列に変換されていることがわかります。

④ ラベルをバイナリクラス行列に変換する

y_train = keras.utils.to_categorical(y_train, num_classes)
y_test = keras.utils.to_categorical(y_test, num_classes)
print('y_train shape:', y_train.shape)
print('y_test shape:', y_test.shape)
  • ラベルデータについても、③のように0と1の行列データに変換します。(https://keras.io/ja/utils/np_utils/)
  • num_classesにはラベルで使用されるデータの最大値が格納されているためnum_classesを要素数とする配列に変換されます。
y_train shape: (8982, 46)
y_test shape: (2246, 46)
  • printの出力は上記になります。最大長が46(num_classes)の配列に変換されていることがわかります。

⑤ モデルを構築する

print('Building model...')
model = Sequential()
model.add(Dense(512, input_shape=(max_words,)))
model.add(Activation('relu'))
model.add(Dropout(0.5))
model.add(Dense(num_classes))
model.add(Activation('softmax'))
  • Kerasが用意するSequentialモデルを使用することで複数の層を重ねるモデルが作成できます。

model.add(Dense(512, input_shape=(max_words,)))

  • 一番はじめの層で入力する次元数を指定します。
  • max_words(=1000)なので1000個の入力を受け取る指定になります
  • 第一引数は出力次元数です(512の意味はまだわかってません..)
  • Kerasでは2層以降の入力数は指定しなくていいようです。
  • Activation('relu')は活性化関数の指定です。(reluはアルゴリズムの方式)
    • 活性化関数とは入力信号に対して出力を求める時にその総和を活性化するため?のアルゴリズムを指すようです。(ココらへんはおいおい深掘りできたらと)
  • Dropout(0.5)過学習を防ぐためにニューロンを無効化する処理です。引数はその割合(この場合は半分の値を捨てている)を指します。
  • Dense(num_classes)で2層目の指定をしています。ここでは出力をnum_classes(=ラベルのクラス数)に指定しています。
  • Activation('softmax')2層目の活性化関数にはsoftmaxというアルゴリズムを使っています。

⑥ 学習の設定

model.compile(loss='categorical_crossentropy',
              optimizer='adam',
              metrics=['accuracy'])
  • 層を構築したモデルに対して学習の設定を行います。
  • lossは損失関数の指定です。categorical_crossentropy交差エントロピー誤差というアルゴリズムを指定しています。
    • 損失関数とは、どれだけ精度が出ているかという指標として使われる関数で、どれだけ正しくないか?という値を求めるためのものです。(この結果が小さい=精度が高いと判断できます)
  • optimizerは、パラメタ最適化のための探索アルゴリズムを指定しています。

  • metricsは評価を行うためのリストの方式?を指定します。

    • 一般的に['accuracy']を指定する?

⑦ 学習の実行

history = model.fit(x_train, y_train,
                    batch_size=batch_size,
                    epochs=epochs,
                    verbose=1,
                    validation_split=0.1)
  • モデル層の構築、学習の設定から実際に学習を行います。
  • 第一引数は、訓練入力データ
  • 第二引数は、訓練ラベル
  • batch_sizeは、同時計算するサンプル数
    • ニューラルネットワークではある一定のデータ量をまとめて処理することができる。まとめて計算することで計算上のコストを削減させている。
  • epochs 学習のサイクル数
  • verbose 結果の出力
  • validation_split ホールドアウト検証として使うデータの割合
    • 標本から一定数の事例を無作為に選択し、残りを訓練事例として使う。

⑧ 損失値の計算

score = model.evaluate(x_test, y_test,
                       batch_size=batch_size, verbose=1)
print('Test score:', score[0])
print('Test accuracy:', score[1])
  • 学習完了したモデルからテストデータを使って損失値を計算する
  • 第一引数は、テスト入力データ
  • 第二引数は、テストラベル
  • batch_sizeは、同時計算サンプル数(fitと同じ)
  • socoreの0番目には損失値(1-損失値)が入っている
  • socoreの1番目には、accuracy(精度)が入っている
    • ⑥のmodel.compileのmetricsで指定した値が入る
  • ※ lossとaccuracyの使い分けがあまりわかっていません。ココらへんもおいおい。
score: 0.889519923517
Test accuracy: 0.793410507569
  • printの出力結果は上記になります。結果としてこのモデルの精度は79%となっています。

まとめ(感想)

ざっくりとやっていることを追っていくとなんとなくどんなことをやっているかは理解できました。 ただ、これらを使って自分がやりたい学習をどう実現するか?どのようなモデルを構築すればよいか?などはまだまだ理解できていません。 自分で学習したいデータを作成する方法など、少しずつ勉強していきたいと思います。

Kerasの環境をMacに構築する

最近、今更ながら機械学習の勉強をしてみようと思い、色々調べていました。
僕は行列計算などの高校数学もほとんどわからないのですが、最近はPythonでのエコシステムがかなり充実してきているようで、頑張れば簡単なもの位は作れるようになるかなと思い、1から勉強しようと思っています。

まずは、Macに環境構築するところから始めます。 機械学習のライブラリとしては、Google製のTensorFlowが有名(僕が知らないだけですが..)ですが、色々みていると、ChainerKerasというのが最近の人気のようです。

Chainerは、日本製とのことで日本語の資料が充実しているらしいのですが、今回はKerasを使ってみます。 理由は、GoogleのTensorFlowをバックエンドに使えるということもあり、今後も成熟が期待できそうだからです。

環境構築の方法は、TensorFlowの公式ページなど色々みてみましたが、Dockerを使ったクリーンな環境構築なども推奨されているようです。

ですが、僕自身Dockerの知識がそこまで豊富でないため、Docker周りのトラブルが出ても面倒なのでとりあえず普通にMacにインストールしてみたいと思います。

Pythonのバージョン管理ツールをインストー

brew install pyenv
brew install pyenv-virtualenv
  • まずはPythonのインストールから行います
  • homebrewを使ってインストールします

パスを通す&初期設定

PYENV_ROOT=~/.pyenv
export PATH=$PATH:$PYENV_ROOT/bin
eval "$(pyenv init -)"
eval "$(pyenv virtualenv-init -)"
  • .bashrc.zshrcにpyenvのパスを通します

Pythonのバージョンインストー

pyenv install anaconda3-4.3.1

virtualenvで環境を作る

conda create -n keras python=3.6.1 anaconda

※ 追記 一番はじめは以下のようにpyenv-virtualenvで環境を作っていましたが、コンソールを切り替えた時などに上手く環境が切り替えられずcondaコマンドを使った方法に変更しました

pyenv virtualenv anaconda3-4.3.1 keras

ローカル環境用のディレクトリを作成する

mkdir keras
cd keras
  • keras用の作業環境を作成するためディレクトリを切ります

ローカル環境を切り替える

pyenv local anaconda3-4.3.1/envs/keras
  • keras用の環境設定をローカル環境として設定します

Tenserflowをインストー

pip install tensorflow

kerasをインストー

pip install keras

その他必要なモジュール

pip install h5py
  • 学習後の結果を保存するためのファイル(h5形式)を扱うライブラリ

設定ファイル

mkdir ~/.keras
echo '{"epsilon": 1e-07, "floatx": "float32", "backend": "tensorflow"}' >> ~/.keras/keras.json
  • backendをtensorflowにするための設定ファイルを作成します。
    • ※ tensorflowがインストールされている状態だとデフォルトでtenserflowになっているようなので必要ないかも

以上で、インストールは完了です。

動作確認

試しにsampleソースを動かしてみます。

kerasでは、幾つかのサンプルソースが提供されています。

その中でも機械学習チュートリアルでもよく出てくるMNISTという手書き数字を認識するためのサンプルを動かしてみました。

https://github.com/fchollet/keras/blob/master/examples/mnist_mlp.py

'''Trains a simple deep NN on the MNIST dataset.
Gets to 98.40% test accuracy after 20 epochs
(there is *a lot* of margin for parameter tuning).
2 seconds per epoch on a K520 GPU.
'''

from __future__ import print_function

import keras
from keras.datasets import mnist
from keras.models import Sequential
from keras.layers import Dense, Dropout
from keras.optimizers import RMSprop


batch_size = 128
num_classes = 10
epochs = 20

# the data, shuffled and split between train and test sets
(x_train, y_train), (x_test, y_test) = mnist.load_data()

x_train = x_train.reshape(60000, 784)
x_test = x_test.reshape(10000, 784)
x_train = x_train.astype('float32')
x_test = x_test.astype('float32')
x_train /= 255
x_test /= 255
print(x_train.shape[0], 'train samples')
print(x_test.shape[0], 'test samples')

# convert class vectors to binary class matrices
y_train = keras.utils.to_categorical(y_train, num_classes)
y_test = keras.utils.to_categorical(y_test, num_classes)

model = Sequential()
model.add(Dense(512, activation='relu', input_shape=(784,)))
model.add(Dropout(0.2))
model.add(Dense(512, activation='relu'))
model.add(Dropout(0.2))
model.add(Dense(10, activation='softmax'))

model.summary()

model.compile(loss='categorical_crossentropy',
              optimizer=RMSprop(),
              metrics=['accuracy'])

history = model.fit(x_train, y_train,
                    batch_size=batch_size,
                    epochs=epochs,
                    verbose=1,
                    validation_data=(x_test, y_test))
score = model.evaluate(x_test, y_test, verbose=0)
print('Test loss:', score[0])
print('Test accuracy:', score[1])

このソースを実行します

python mnist_mlp.py

以下が結果です。

Using TensorFlow backend.
60000 train samples
10000 test samples
_________________________________________________________________
Layer (type)                 Output Shape              Param #
=================================================================
dense_1 (Dense)              (None, 512)               401920
_________________________________________________________________
dropout_1 (Dropout)          (None, 512)               0
_________________________________________________________________
dense_2 (Dense)              (None, 512)               262656
_________________________________________________________________
dropout_2 (Dropout)          (None, 512)               0
_________________________________________________________________
dense_3 (Dense)              (None, 10)                5130
=================================================================
Total params: 669,706
Trainable params: 669,706
Non-trainable params: 0
_________________________________________________________________
Train on 60000 samples, validate on 10000 samples
Epoch 1/20
2017-06-22 12:47:14.203012: W tensorflow/core/platform/cpu_feature_guard.cc:45] The TensorFlow library wasn't compiled to use SSE4.2 instructions, but these are available on your machine and could speed
up CPU computations.
2017-06-22 12:47:14.203031: W tensorflow/core/platform/cpu_feature_guard.cc:45] The TensorFlow library wasn't compiled to use AVX instructions, but these are available on your machine and could speed up
CPU computations.
2017-06-22 12:47:14.203037: W tensorflow/core/platform/cpu_feature_guard.cc:45] The TensorFlow library wasn't compiled to use AVX2 instructions, but these are available on your machine and could speed up CPU computations.
2017-06-22 12:47:14.203042: W tensorflow/core/platform/cpu_feature_guard.cc:45] The TensorFlow library wasn't compiled to use FMA instructions, but these are available on your machine and could speed up
CPU computations.
60000/60000 [==============================] - 8s - loss: 0.2422 - acc: 0.9254 - val_loss: 0.1386 - val_acc: 0.9573
Epoch 2/20
60000/60000 [==============================] - 6s - loss: 0.1014 - acc: 0.9699 - val_loss: 0.0768 - val_acc: 0.9771
Epoch 3/20
60000/60000 [==============================] - 6s - loss: 0.0753 - acc: 0.9771 - val_loss: 0.0798 - val_acc: 0.9758
Epoch 4/20
60000/60000 [==============================] - 6s - loss: 0.0615 - acc: 0.9815 - val_loss: 0.0732 - val_acc: 0.980
Epoch 5/20
60000/60000 [==============================] - 6s - loss: 0.0495 - acc: 0.9850 - val_loss: 0.0902 - val_acc: 0.9783
Epoch 6/20
60000/60000 [==============================] - 6s - loss: 0.0443 - acc: 0.9866 - val_loss: 0.0860 - val_acc: 0.9797
Epoch 7/20
60000/60000 [==============================] - 6s - loss: 0.0380 - acc: 0.9883 - val_loss: 0.0719 - val_acc: 0.9834
Epoch 8/20
60000/60000 [==============================] - 7s - loss: 0.0351 - acc: 0.9900 - val_loss: 0.0869 - val_acc: 0.9822
Epoch 9/20
60000/60000 [==============================] - 8s - loss: 0.0327 - acc: 0.9906 - val_loss: 0.0764 - val_acc: 0.9827
Epoch 10/20
60000/60000 [==============================] - 7s - loss: 0.0308 - acc: 0.9911 - val_loss: 0.0786 - val_acc: 0.9836
Epoch 11/20
60000/60000 [==============================] - 9s - loss: 0.0285 - acc: 0.9918 - val_loss: 0.0814 - val_acc: 0.9835
Epoch 12/20
60000/60000 [==============================] - 7s - loss: 0.0248 - acc: 0.9930 - val_loss: 0.0928 - val_acc: 0.9824
Epoch 13/20
60000/60000 [==============================] - 7s - loss: 0.0259 - acc: 0.9927 - val_loss: 0.1057 - val_acc: 0.9823
Epoch 14/20
60000/60000 [==============================] - 7s - loss: 0.0231 - acc: 0.9933 - val_loss: 0.1030 - val_acc: 0.9820
Epoch 15/20
60000/60000 [==============================] - 7s - loss: 0.0232 - acc: 0.9939 - val_loss: 0.1116 - val_acc: 0.9815
Epoch 16/20
60000/60000 [==============================] - 7s - loss: 0.0204 - acc: 0.9947 - val_loss: 0.1055 - val_acc: 0.9829
Epoch 17/20
60000/60000 [==============================] - 6s - loss: 0.0217 - acc: 0.9947 - val_loss: 0.1281 - val_acc: 0.9821
Epoch 18/20
60000/60000 [==============================] - 7s - loss: 0.0197 - acc: 0.9950 - val_loss: 0.1176 - val_acc: 0.9839
Epoch 19/20
60000/60000 [==============================] - 7s - loss: 0.0229 - acc: 0.9942 - val_loss: 0.1120 - val_acc: 0.9831
Epoch 20/20
60000/60000 [==============================] - 7s - loss: 0.0228 - acc: 0.9944 - val_loss: 0.1078 - val_acc: 0.9834
Test loss: 0.107835494385
Test accuracy: 0.9834

と動いているようです。

The TensorFlow library wasn't compiled to use AVX instructions, but these are available on your machine and could speed up
CPU computations.

あたりの警告は、ソースからコンパイルしていないTenserFlowの場合CPUの拡張オプションが使えないのでスピードアップができないみたいなことみたいです。

とりあえず、よくわかってませんが動く環境をつくるところまでは成功しました。

ここから徐々に機械学習への理解を深めていきたいと思います。

参考

Installing TensorFlow on Mac OS X  |  TensorFlow

pyenvとanacondaを共存させる時のactivate衝突問題の回避策3種類 - Qiita

Python: Keras/TensorFlow の学習を CPU の拡張命令で高速化する (Mac OS X) - CUBE SUGAR CONTAINER

独自のDockerImageを作成する

Dockerでは、自分で独自のimageを作成することが可能です。 独自のdocker imageを作成する方法は主に2つあります。

docker container commitコマンドを使う ② Dockerfileからビルドする

今回は、①の起動中のDockerコンテナから新しいイメージを作成する方法を試してみます。

※ 実行環境は、Docker for Macを使っています。 ※ 環境構築については過去記事のとおりです。

inon29.hateblo.jp

docker container commitコマンドを使う

ベースイメージの準備

今回はnginxのイメージを元に独自のイメージを作成してみます。 まずは、ベースとなるnginxのimageをDocker Hubからダウンロードします。

docker image pull nginx:latest
  • docker image pull <イメージ名>:<タグ名>
  • ※ タグ名をlatestにすると最新のバージョンを取得

イメージを確認してみます。

docker images
REPOSITORY          TAG                 IMAGE ID            CREATED             SIZE
nginx               latest              46102226f2fd        4 days ago          109 MB

取得したイメージからコンテナを起動します。

docker run -d -p 80:80 --name webserver nginx
  • docker run -d -p <ポート> --name <コンテナ名> <イメージ名>
  • -d: コンテナをバックグラウンドで起動する
  • -p: ポート指定、<ホストのポート>:<コンテナのポート>で割当を行う。 例: <80>:<12345> コンテナの12345ポートをホストの80ポートに割り当てる 
  • --name: コンテナに名前を割り当てる

localhostにアクセスして確認します。

スクリーンショット 2017-04-30 16.30.15.png

nginxが起動してデフォルトのindex.htmlファイルが表示されていることが確認できました。

コンテナの内容を編集

dockerのコンテナにアクセスしてこのindexファイルを編集してみます。

docker exec -it webserver /bin/bash
  • docker exec -it <コンテナ名> <コマンド>
  • -i: コマンド入力後にコンテナの標準入力をホストの標準入力と接続する
  • -t: コンテナ内の仮想端末と、Dockerホストの標準出力と接続する
cd /usr/share/nginx/html
echo "<h1>Welcome my nginx</h1>" > index.html

localhostに再度アクセスするとindex.htmlの内容が変更されていることが確認できます。

スクリーンショット 2017-04-30 17.03.54.png

docker imageを作成

index.htmlを編集した状態の今のコンテナからimageを作成します。

docker container commit webserver mynginx:latest
  • docker container commit <コンテナ名> <イメージ名>:<タグ>

イメージを確認してみます

docker images
REPOSITORY          TAG                 IMAGE ID            CREATED             SIZE
mynginx             latest              fa7dff65a5ba        5 seconds ago       109 MB
nginx               latest              46102226f2fd        4 days ago          109 MB

起動中のコンテナを削除して今作ったイメージからコンテナを起動します。

docker stop webserver
docker rm webserver
  • docker stop <コンテナ名>: コンテナを停止
  • docker rm <コンテナ名>: コンテナの削除
docker run -d -p 80:80 --name myserver mynginx

localhostにアクセスして確認します。

スクリーンショット 2017-04-30 17.03.54.png

先程に編集した状態でイメージが起動していることを確認できました。