Murayama blog.

プログラミング教育なブログ

強化学習 - Tic-Tac-Toe

強化学習 - Tic-Tac-Toe

三目並べ、マルバツゲーム、Tic-Tac-Toeというそうです。強化学習(Q-Learning)のまとめとしてチャレンジしてみました。Googleで「Tic-Tac-Toe」と検索すると三目並べで遊べます。

先に結果

ランダムな相手(後手)には80%近い確率で勝てるようになりました。でも実際に対戦してみると「ちょっとかしこいかな?」くらいの印象です。今日の勢いで作ったのでプログラムの細かいところに不備があるかも?しれません。。あまり参考にならないかも。

設定など

  • アルゴリズム
    • Q-Learning
  • 報酬
    • 勝ち:1
    • 負け:-1
    • 引き分け:0
      • 引き分けも多いので、報酬としてプラスマイナスがあるのも良いのかも。
  • 状態
    • 3**9 = 19683とおり
  • 行動
    • 9マスあるので9とおり
      • キーボード入力時は0〜8
      • 既に入力済みの場所を選択した場合は、ランダムで配置するようにしています。
        • このせいで勝率はやや落ちるかも。
  • パラメータなど
    • 学習回数(エピソード数):2,000,000くらい(他のサイトを見るともっと少なく良さそう)
    • 学習率:0.1
    • gamma:0.9

所感

  • マルバツゲーム本体のプログラムと、強化学習プログラムのインタフェース設計に少し迷った。
    • OpenAI Gymのインタフェースに習って、マルバツゲームにstep関数を実装した。
  • 学習後の勝率が60%で停滞した。
    • εの更新式を間違っていたため、Qテーブルの値に依存した学習を繰り返していた模様
  • そもそもマルバツゲーム本体のロジックに不備があったり。
    • テストコードを書こう。
  • Qテーブルの初期値も迷った。
    • -1〜1のようなランダムな値も試したが、あまり効果はなさそうだったのでとりあえず0にした。
  • 常に先手("x")をAIとした。
    • 学習済みのAIを後手で起動すると精度が下がる。

作成したファイル

まずは強化学習用のプログラムです。

  • tictactoe.py
    • マルバツゲーム本体プログラム
  • train_tictactoe.py
    • Q-Learningで学習するプログラム
    • ランダムな打ち手(後手)と繰り返し対決します。
    • 学習後Qテーブルの内容をnumpyのファイルとして出力します。

以下は学習済みプログラムを動作させるためのプログラムです。

  • play_test_tictactoe.py
    • 学習済みのQテーブルを使って検証するプログラム
  • play_tictactoe.py
    • キーボード入力で対決できるプログラム

tictactoe.py

  • マルバツゲーム本体プログラム
  • 先手、後手ともにランダムで動作します。
import numpy as np

class TicTacToe:
    def __init__(self, printable=False):
        self.reset(printable)

    def show(self):
        if self.printable:
            print(self.board[0], "|", self.board[1], "|" ,self.board[2])
            print("----------" )
            print(self.board[3], "|", self.board[4], "|" ,self.board[5])
            print("----------" )
            print(self.board[6], "|", self.board[7], "|" ,self.board[8])

    def reset(self, printable=None) :
        self.board = [" ", " ", " ", " ", " ", " ", " ", " ", " "]
        self.player1 = "x"
        self.player2 = "o"
        self.player = self.player1
        self.done = False
        if printable is not None:
            self.printable = printable
        return self.getObservation()

    def put(self, number) :
        if number < 0 or 8 < number:
            return False
        if self.board[number] != " ":
            return False
        self.board[number] = self.player
        return True

    def judge(self):
        if self.win() or self.draw():
            self.done = True
        return self.done

    def win(self):
        return self.player == self.board[0] and self.player == self.board[1] and self.player == self.board[2] \
                or self.player == self.board[3] and self.player == self.board[4] and self.player == self.board[5] \
                or self.player == self.board[6] and self.player == self.board[7] and self.player == self.board[8] \
                or self.player == self.board[0] and self.player == self.board[3] and self.player == self.board[6] \
                or self.player == self.board[1] and self.player == self.board[4] and self.player == self.board[7] \
                or self.player == self.board[2] and self.player == self.board[5] and self.player == self.board[8] \
                or self.player == self.board[0] and self.player == self.board[4] and self.player == self.board[8] \
                or self.player == self.board[2] and self.player == self.board[4] and self.player == self.board[6]

    def draw(self):
        return (" " in self.board) == False

    def changePlayer(self):
        if self.player == self.player1:
            self.player = self.player2
        else:
            self.player = self.player1

    def printStart(self):
        if self.printable:
            print("Tic Tac Toe Start!", flush=True)

    def printEnd(self):
        if self.printable:
            print("Tic Tac Toe End!", flush=True)

    def printChoice(self):
        if self.printable:
            print("Choice! (0,1,2,3,4,5,6,7,8)", flush=True)

    def printChoiceInvalid(self):
        if self.printable:
            print("Your choice is invalid", flush=True)

    def printWin(self):
        if self.printable:
            print(self.player, "Win!", flush=True)

    def printDraw(self):
        if self.printable:
            print("Draw!", flush=True)

    def printSpace(self):
        if self.printable:
            print(flush=True)

    def getRandomAction(self):
        return np.random.randint(0, 9)

    def start(self):
        self.printStart()
        while True:
            action = self.getRandomAction()
            (_, _, done, _) = self.step(action)
            self.show()
            self.printSpace()
            if done:
                self.show()
                if self.win():
                    self.printWin()
                else:
                    self.printDraw()
                break
        self.printEnd()

    def step(self, action):
        while True:
            if self.put(action) :
                break
            else:
                action = self.getRandomAction()
        reward = 0
        if self.judge() :
            if self.win():
                if self.player == self.player1:
                    reward = 1
                else:
                    reward = -1
            else:
                reward = 0
        else:
            self.changePlayer()
            reward = 0
        return (self.getObservation(), reward, self.done, {})

    def getObservation(self):
        return (self.board, self.player, self.player1, self.player2)

if __name__ == "__main__":
    ttt = TicTacToe(True)
    ttt.start()

train_tictactoe.py

import numpy as np
from tictactoe import TicTacToe

import numpy as np

def get_action(q_table, state, epsilon):
    if np.random.uniform(0, 1) <= epsilon:
        return np.random.randint(0, 9)
    else:
        a = np.where(q_table[state] == q_table[state].max())[0]
        return np.random.choice(a)

def board_to_state(board):
    state = 0
    for i in range(0, 9):
        if board[i] == 'o':
            state = state + 3**i * 2
        elif board[i] == 'x':
            state = state + 3**i * 1
        else:
            state = state + 3**i * 0
    return state

def update_q_learning(state, ation, reward, next_state, q_table):
    eta = 0.1
    gamma = 0.9
    if reward != 0:
        q_table[state,action] = q_table[state,action] + \
                                eta * (reward - q_table[state,action])
    else:
        q_table[state,action] = q_table[state,action] + \
                                eta * (reward + gamma * np.max(q_table[state,:]) - q_table[state,action])
    return q_table

np.set_printoptions(precision=6, suppress=True)

prefix = "q_table_data_"
episode = 100_000
# episode = 2_000_000

# q_table = np.random.uniform(low=-1, high=1, size=(3**9, 9))
q_table = np.zeros((3**9, 9))
# q_table = np.load('q_table_dataa_5000000.npy')

ttt = TicTacToe()
threshold = 10000
initial_epsilon = 0.5
win = 0
draw = 0
for i in range(1, episode):
    epsilon = initial_epsilon * (episode - i) / episode
    my_turn = True
    observation = ttt.reset()
    while True:
        state = board_to_state(observation[0])
        action = None
        if my_turn:
            action = get_action(q_table, state, epsilon)
        else:
            action = ttt.getRandomAction()

        (observation, reward, done, _) = ttt.step(action)

        if my_turn:
            next_state = board_to_state(observation[0])
            q_table = update_q_learning(state, action, reward, next_state, q_table)

        if done:
            if ttt.win():
                if ttt.player == ttt.player1:
                    win = win + 1
            else:
                draw = draw + 1
            break

        my_turn = not my_turn
    if i % threshold == 0:
        lose = threshold - win - draw
        print("episode", i, "/", episode, "win", win, "draw", draw, "lose", lose)
        win = 0
        draw = 0

print(q_table)
np.save(prefix + str(episode), q_table)

play_test_tictactoe.py

  • 学習結果を確認するプログラムです。
  • 学習済みのQテーブルをロードして、10000回の試行結果を出力します。
from tictactoe import TicTacToe

import numpy as np

def get_action(q_table, state):
    a = np.where(q_table[state] == q_table[state].max())[0]
    return np.random.choice(a)

def board_to_state(board):
    state = 0
    for i in range(0, 9):
        if board[i] == 'o':
            state = state + 3**i * 2
        elif board[i] == 'x':
            state = state + 3**i * 1
        else:
            state = state + 3**i * 0
    return state

max_episode = 10000
q_table = np.load('q_table_data_5000000.npy')
ttt = TicTacToe(False)
win = 0
draw = 0
ttt.printStart()
for i in range(1, max_episode):
    my_turn = True
    observation = ttt.reset()
    while True:
        state = board_to_state(observation[0])
        action = None
        if my_turn:
            action = get_action(q_table, state)
            # action = np.random.randint(0, 9)
        else:
            ttt.printChoice()
            action = np.random.randint(0, 9)
        (observation, reward, done, _) = ttt.step(action)
        ttt.show()
        ttt.printSpace()
        ttt.printSpace()
        if done:
            if ttt.win():
                ttt.printWin()
                if ttt.player == ttt.player1:
                    win = win + 1
            else:
                ttt.printDraw()
                draw = draw + 1
            break
        my_turn = not my_turn
print("episode", max_episode, "win", win, "draw", draw, "lose", max_episode - win - draw)

play_tictactoe.py

  • ユーザと対戦用のプログラムです。
from tictactoe import TicTacToe

import numpy as np

def get_action(q_table, state):
    a = np.where(q_table[state] == q_table[state].max())[0]
    return np.random.choice(a)

def board_to_state(board):
    state = 0
    for i in range(0, 9):
        if board[i] == 'o':
            state = state + 3**i * 2
        elif board[i] == 'x':
            state = state + 3**i * 1
        else:
            state = state + 3**i * 0
    return state

q_table = np.load('q_table_5000000.npy')

ttt = TicTacToe(True)
win = 0
draw = 0
ttt.printStart()
my_turn = True
observation = ttt.reset()
while True:
    state = board_to_state(observation[0])
    action = None
    if my_turn:
        action = get_action(q_table, state)
        # action = np.random.randint(0, 9)
    else:
        ttt.printChoice()
        action = int(input().strip())
        # action = np.random.randint(0, 9)
    (observation, reward, done, _) = ttt.step(action)
    ttt.show()
    ttt.printSpace()
    ttt.printSpace()
    if done:
        if ttt.win():
            ttt.printWin()
            if ttt.player == ttt.player1:
                win = win + 1
        else:
            ttt.printDraw()
            draw = draw + 1
        break
    my_turn = not my_turn

参考

qiita.com

イプシロンの更新ロジックを参考にさせてもらいました。

data.gunosy.io

改めて読むとわかりやすいです。既に入力済みの場所を選択した場合の制御も入れると勝率上がりそう。エピソード数は10000回で良さそう。

強化学習の始め方

普段やらない、強化学習について少し勉強したのでメモしておきます。

参考書籍

先に参考書籍の紹介です。

Pythonによる深層強化学習入門 ChainerとOpenAI Gymではじめる強化学習

Pythonによる深層強化学習入門 ChainerとOpenAI Gymではじめる強化学習

DeepLearningの経験がある人にオススメです。コードによる説明が多く、プログラマー向けの書籍という感じでした。一冊目にちょうど良かったです。個人的にはQ-Learningのサンプルがとてもわかりやすかったです。OpenAI Gymのサンプルも豊富で手を動かす勉強に向いています。後半はRaspberry PIArduinoを使ったデモも載っています。

つくりながら学ぶ! 深層強化学習 ~PyTorchによる実践プログラミング~

つくりながら学ぶ! 深層強化学習 ~PyTorchによる実践プログラミング~

こちらも良書です。とても説明が丁寧で読みやすいです。技術の背景や用語の説明もしっかりしているので勉強になります。順番的には2冊目にちょうど良い印象です。あとサンプルプログラムにはアニメーションなども実装されているので、直感的でわかりやすいです。著者の方はブログやQiita等での情報発信も凄くてファンになりました。こんな本書けるようになりたいです。

あともう一冊手元にあるのですが、まだしっかり読めていないので割愛です。

今回勉強するまで、強化学習についてはなんとなく「準備が大変そう」みたいに思っていたのですが、実際やってみるとそうでもなく、今は良い書籍がたくさんあって、ライブラリも豊富で、とても学びやすくなっている印象を受けました。

強化学習の始め方

1週間勉強した感想です。私の場合は上記の2冊が学び始めにちょうど良かったです。

これから強化学習を学ぶ方は、強化学習について全体像をザックリ見ておくと良いです。強化学習にもいろんなアルゴリズムがあります。以下の記事が詳しいです。

qiita.com

アルゴリズムを俯瞰してみると名前にインパクトのあるDQNとか有名ですね。私のように、これから強化学習について学び始める人にとってはQ-Learning、SARSA、方策勾配法あたりから手を付けてみるのが良さそうです。

何を題材にするか

教師あり学習、たとえば画像認識の場合だとMNISTデータセット、簡単な分類問題、クラスタリングの場合はirisデータセットみたいに、すぐに学習に使えるデータセットがあると便利です。

強化学習においてはデータセット、というより、もう少し大きな枠組みになるので「環境」といった方で良いでしょうか。OpenAI Gymを使うと倒立振子(CartPole)やスペースインベーダーブロック崩しなどを題材に強化学習を始めることができます。

https://gym.openai.com/

OpenAI Gymの環境設定も難しくはありませんが、Pythonのライブラリ管理の知識が必要だったり、環境設定特有のトラブルがついてくるので、もっと手軽に始めれる題材ないのかなーと思っていたらやっぱりあるんですね。「Skinner箱」というのが強化学習の入り口のようです。

ja.wikipedia.org

スキナー箱

マウス(ネズミ)が餌をとるまでの物語です。スキナー箱の中には1匹のマウスがおり、餌を獲得するための2つのスイッチがあります。

  • 電源スイッチ
    • 押すたびにON/OFFが切り替わる
  • 餌スイッチ
    • 電源スイッチがONのときに餌が出る

f:id:yamasahi:20190917171418p:plain

スキナー箱には2つの状態(State)と2つの行動(Action)があります。

f:id:yamasahi:20190917171430p:plain

初期状態において、マウスはどちらのスイッチを押したら餌が出るのかわかないため、ランダムにスイッチを押すことになります。運が良ければ「電源スイッチ」=>「餌スイッチ」と押すことで、最短2ステップで餌を獲得することができます。

マウスは繰り返し餌の獲得に取り組むことで、学習によってテーブルの値を更新していきます。

f:id:yamasahi:20190917171446p:plain

以降は、参考書籍のプログラムを参考にスキナー箱について、3つのアルゴリズムの解法をまとめました。

方策勾配法(Policy Gradient Method)

方策(Policy)とは、エージェントがどのように振る舞うかを決めるルールのことです。エージェントとは今回でいうとマウス(の意思決定する部分)のことです。方策は表形式で表現したり、関数で表現したりします。ここでは状態と行動の2x2の表形式で方策を管理します。

import numpy as np

def to_pi_softmax(theta):
    pi = np.zeros((theta.shape))
    exp_theta = np.exp(1.0 * theta)
    for i in range(theta.shape[0]):
        pi[i,:] = exp_theta[i,:] / np.sum(exp_theta[i,:])
    return pi

def get_action(pi, state):
    return np.random.choice([0, 1], p=pi[state])

def get_next_state(state, action):
    if state == 0 and action == 0:
        return 1
    elif state == 0 and action == 1:
        return 0
    elif state == 1 and action == 0:
        return 0
    else:
        return 1

def challenge(pi):
    state = 0
    history = []
    while True:
        action = get_action(pi, state)
        history.append([state, action])
        if state == 1 and action == 1:
            break
        state = get_next_state(state, action)
    return history

def update_policy_gradient(theta, pi, history):
    delta_theta = theta.copy()
    t = len(history)
    for i in range(theta.shape[0]):
        for j in range(theta.shape[1]):
            n_i = len([sa for sa in history if sa[0] == i])
            n_ij = len([sa for sa in history if sa == [i, j]])
            delta_theta[i, j] = (n_ij - pi[i, j] * n_i) / t
    return theta + 0.25 * delta_theta

theta = np.array([[1.0, 1.0], [1.0, 1.0]])
pi = to_pi_softmax(theta)
print(pi)
for i in range(1, 100):
    history = challenge(pi)
    new_theta = update_policy_gradient(theta, pi, history)
    new_pi = to_pi_softmax(new_theta)
    print(len(history), end=" ", flush=True)
    if i % 10 == 0:
        print("\n", new_pi)
    theta = new_theta
    pi = new_pi
print("\n", pi)

方策はPolicyのPをギリシャ文字のπとして表現することが一般的なようです。上記のプログラムの場合、thetaが学習で更新されるパラメータで、方策テーブル(π)では状態ごとにsoftmax関数を使うことで、各行動を割合として管理しています。

実行結果

$ python skinner_policy_gradient.py
[[0.5 0.5]
 [0.5 0.5]]
2 10 6 4 7 2 7 4 7 2
 [[0.5055308  0.4944692 ]
 [0.39641196 0.60358804]]
9 9 2 4 11 7 4 4 2 5
 [[0.57791613 0.42208387]
 [0.3823193  0.6176807 ]]
3 7 10 2 4 4 8 2 2 2
 [[0.6332647  0.3667353 ]
 [0.30543971 0.69456029]]
2 5 2 7 9 8 6 3 13 2
 [[0.68658989 0.31341011]
 [0.33609984 0.66390016]]
5 4 7 2 2 2 2 12 2 6
 [[0.69865129 0.30134871]
 [0.28321905 0.71678095]]
2 2 5 3 2 3 3 2 5 2
 [[0.72277102 0.27722898]
 [0.21997818 0.78002182]]
5 4 2 4 2 2 3 4 2 4
 [[0.72245457 0.27754543]
 [0.2008336  0.7991664 ]]
4 2 2 4 2 2 2 2 2 2
 [[0.82114666 0.17885334]
 [0.16544254 0.83455746]]
6 3 4 2 8 4 2 2 2 2
 [[0.85727318 0.14272682]
 [0.18526107 0.81473893]]
2 2 2 2 2 2 4 2 4
 [[0.86500809 0.13499191]
 [0.15425348 0.84574652]]

実行結果には方策テーブルとマウスの試行回数を10回ずつ出力しています。学習が進むに連れてマウスは試行回数2回で餌にたどり着けるようになります。方策テーブルも状態0においては、行動0をとるようになり、状態1においては行動1をとるように学習できています。あと方策勾配法は強化学習でよく聞く「報酬」とか出てこないんですね。

SARSA

おまけでSARSAに置き換えてみました。SARSAや後のQ-Learningは価値反復法(value iteration)に分類されるようです(この辺、ググるといろんな説明がある)。価値反復法では、報酬や価値(状態価値、行動価値)、マルコフ決定過程やベルマン方程式というキーワードが出てきます。また時間があるときにまとめるかも。。

import numpy as np

def get_action(q_table, state, epsilon):
    if np.random.uniform(0, 1) <= epsilon:
        return np.random.choice([0, 1])
    else:
        if q_table[state, 0] == q_table[state, 1]:
            return np.random.choice([0, 1])
        return np.argmax(q_table[state,:])

def get_next_state_and_reward(state, action):
    if state == 0 and action == 0:
        return (1, 0)
    elif state == 0 and action == 1:
        return (0, 0)
    elif state == 1 and action == 0:
        return (0, 0)
    else:
        return (1, 1)

def update_sarsa(state, action, reward, next_state, next_action, q_table):
    eta = 0.1
    gamma = 0.9
    if reward == 1:
        q_table[state,action] = q_table[state,action] + eta * (reward - q_table[state,action])
    else:
        q_table[state,action] = q_table[state,action] + eta * (reward + gamma * q_table[next_state,next_action] - q_table[state,action])
    return q_table

def challenge(q_table, epsilon):
    state = 0
    action = get_action(q_table, state, epsilon)
    history = []
    while True:
        history.append([state, action])
        [next_state, reward] = get_next_state_and_reward(state, action)
        next_action = get_action(q_table, next_state, epsilon)
        q_table = update_sarsa(state, action, reward, next_state, next_action, q_table)
        if reward == 1:
            break
        state = next_state
        action = next_action
    return (q_table, history)

np.set_printoptions(precision=6, suppress=True)

q_table = np.zeros((2, 2))
print(q_table)
epsilon = 1.0
for i in range(1, 101):
    epsilon = epsilon * 0.9
    [q_table, history] = challenge(q_table, epsilon)
    print(len(history), end=" ", flush=True)
    if i % 10 == 0:
        print("\n", q_table)

このプログラムでは行動価値関数を変数q_tableで管理しています。q_tableは2x2の状態と行動の表データで初期値を0としています。学習が進むにつれてq_tableの値が調整されていきます。

SARSAの名前の由来はSARSAの更新式に必要なState、Action、Reward、next-State、next-Actionの5つの頭文字です。ここではSARSAの更新式であるupdate_sarsa関数の引数もその順で定義しています。

SARSAでは基本的にはq_tableに従って、ある状態における行動を決定するわけですが、一定の割合でランダムな行動をとるようにしています。これはε-greedy法という考え方に従うもので、より良い行動を探すための仕組みです。強化学習の世界には「探索と利用のトレードオフ(exploitation-exploration trade-offs)」という言葉もあるようです。深いです。

ランダムに動作するためのepsilonの割合は繰り返し(エピソード)ごとに0.5を掛けるものが多くありましたが、ここでは学習の様子(失敗するケース)を強調するために0.9を掛けています。

実行結果

$ python skinner_sarsa.py
[[0. 0.]
 [0. 0.]]
8 5 3 2 7 4 4 2 4 2
 [[0.216381 0.010251]
 [0.020069 0.651322]]
2 2 2 2 2 2 2 2 2 2
 [[0.54006  0.010251]
 [0.020069 0.878423]]
2 2 2 2 2 2 2 2 2 2
 [[0.732106 0.010251]
 [0.020069 0.957609]]
2 2 2 2 3 2 2 2 2 2
 [[0.826678 0.07931 ]
 [0.020069 0.985219]]
2 2 2 2 2 2 2 2 2 2
 [[0.86928  0.07931 ]
 [0.020069 0.994846]]
2 2 2 2 2 2 2 2 2 2
 [[0.887492 0.07931 ]
 [0.020069 0.998203]]
2 2 2 2 2 2 2 2 2 2
 [[0.895012 0.07931 ]
 [0.020069 0.999373]]
2 2 2 2 2 2 2 2 2 2
 [[0.898042 0.07931 ]
 [0.020069 0.999782]]
2 2 2 2 2 2 2 2 2 2
 [[0.899241 0.07931 ]
 [0.020069 0.999924]]
2 2 2 2 2 2 2 2 2 2
 [[0.899709 0.07931 ]
 [0.020069 0.999973]]

q_tableの初期値は0としています。学習が進むにつれて、q_table[0][0]やq_table[1][1]の値が大きくなっているのがわかります。余談ですが、方策勾配法の方策piと比較すると、q_tableの状態ごとの値は割合ではないので加算したら1になるわけではないようです。

また学習が進むにつれて、ランダムに動作する割合を示すepsilonが小さくなるので不規則な行動はとらなくなります。そのためq_tableに従って最小の2ステップで餌にたどり着くことができています。

Q-Learning

さいごにQ-Learningです。SARSAとよく似ていて、行動価値関数(変数q_table)の更新式が少し異なります。SARSAでは更新式にnext-State、next-Actionの2つが必要でしたが、Q-Learningではnext-Stateにおける行動(Action)の中から値の最大値のものを選択するようにします。

import numpy as np

def get_action(q_table, state, epsilon):
    if np.random.uniform(0, 1) <= epsilon:
        return np.random.choice([0, 1])
    else:
        if q_table[state, 0] == q_table[state, 1]:
            return np.random.choice([0, 1])
        return np.argmax(q_table[state,:])

def get_next_state_and_reward(state, action):
    if state == 0 and action == 0:
        return (1, 0)
    elif state == 0 and action == 1:
        return (0, 0)
    elif state == 1 and action == 0:
        return (0, 0)
    else:
        return (1, 1)

def update_q_learning(state, action, reward, next_state, q_table):
    eta = 0.1
    gamma = 0.9
    if reward == 1:
        q_table[state,action] = q_table[state,action] + eta * (reward - q_table[state,action])
    else:
        q_table[state,action] = q_table[state,action] + eta * (reward + gamma * np.max(q_table[next_state,:]) - q_table[state,action])
    return q_table

def challenge(q_table, epsilon):
    state = 0
    action = get_action(q_table, state, epsilon)
    history = []
    while True:
        history.append([state, action])
        [next_state, reward] = get_next_state_and_reward(state, action)
        q_table = update_q_learning(state, action, reward, next_state, q_table)
        if reward == 1:
            break
        state = next_state
        action = get_action(q_table, state, epsilon)
    return (q_table, history)

np.set_printoptions(precision=6, suppress=True)

q_table = np.zeros((2, 2))
print(q_table)
epsilon = 1.0
for i in range(1, 101):
    epsilon = epsilon * 0.9
    [q_table, history] = challenge(q_table, epsilon)
    print(len(history), end=" ", flush=True)
    if i % 10 == 0:
        print("\n", q_table)

実行結果

$ python skinner_q_learning.py
[[0. 0.]
 [0. 0.]]
2 3 6 2 3 2 6 2 2 3
 [[0.270356 0.047601]
 [0.01882  0.651322]]
3 3 2 2 2 2 2 2 2 2
 [[0.55888  0.08763 ]
 [0.01882  0.878423]]
2 2 2 2 2 2 4 4 3 2
 [[0.759274 0.145173]
 [0.136828 0.957609]]
2 2 2 2 2 2 2 2 2 2
 [[0.836151 0.145173]
 [0.136828 0.985219]]
2 2 2 2 2 2 2 2 2 2
 [[0.872583 0.145173]
 [0.136828 0.994846]]
2 2 2 2 2 2 2 2 2 2
 [[0.888643 0.145173]
 [0.136828 0.998203]]
2 2 2 2 2 2 2 2 2 2
 [[0.895414 0.145173]
 [0.136828 0.999373]]
2 2 2 2 2 2 2 2 2 2
 [[0.898182 0.145173]
 [0.136828 0.999782]]
2 2 2 2 2 2 2 2 2 2
 [[0.89929  0.145173]
 [0.136828 0.999924]]
2 2 2 2 2 2 2 2 2 2
 [[0.899726 0.145173]
 [0.136828 0.999973]]

結果はSARSAのときと同じようにq_tableの値が更新されているのがわかります。

その次の勉強

とりあえずはOpenAI Gymの題材にチャレンジするのが良さそうです。有名な倒立振子(CartPole)については書籍やインターネット上でサンプルもたくさん紹介されています。他にもToy textなるものもありました。私もいくつか触ってみましたが、FrozenLake問題というのは勉強するのにちょうど良い感じがしました。

https://gym.openai.com/envs/#toy_text

他にも調べているとブロック崩しゲームも自力で解けるみたいです。GPUマシンもクラウドでどうにかなるし。

今後は自分で何か強化学習の題材を作ってみようと思っています。面白いかどうかは別として、マルバツゲームや五目並べなどにチャレンジしてみようかと思っています。Q-Learningだと状態や行動、報酬の設計をゼロからできるようになれば世界が広がりそうです。

あとはアルゴリズムについてはコードだけでなく数式による理解も大事ですね。この辺はコツコツと。それからDQNなどの深層学習に取り組むのも面白そうです。今まではKerasしか使ったことなかったですが最近はPyTorchが良いみたいです。

まとめ

普段やらないことをやってみました。勉強になりました。

超速習 - はじめてのPHPプログラミング

最速?でPHPプログラミングの基礎を学ぶためのガイドです。できるだけ短いコードでプログラミングの基礎となる変数や配列、制御構文、関数を紹介してみました。30分くらいで一通り紹介するイメージで。

開発環境としてはMacを想定しています。最近のMacはデフォルトでPHPがインストールされているので、AtomVSCodeのようなテキストエディタとターミナル*1を起動すればすぐにPHPの学習を始めることができます。*2

サンプルプログラム

ここでは以下のサンプルプログラムを取り上げます。最速で。

  1. データの出力
  2. 変数
  3. 配列(添字配列)
  4. 制御構文(for文)
  5. 制御構文(if文)
  6. 配列(連想配列
  7. 関数
  8. まとめ

プログラミング初学者にとっては配列や関数など躓きやすいポイントがいくつかありますが、上から順番に学習を進めていけばどこが苦手かチェックできると思います。

1 データの出力(sample1.php

次のプログラムは画面(ターミナル)にHello PHPと出力するプログラムです。ファイル名はsample1.phpとして任意のフォルダに保存します。

以降のサンプルプログラムはMac上で/Users/murayama/Desktop/php-basicフォルダに保存したものとします。

<?php
echo "Hello PHP";
?>

PHPで画面にデータを出力するにはecho命令を使います。上記のように記述すれば画面にHello PHPと出力されます。

次にターミナル(Windowsの場合はコマンドプロンプト)を開きます。カレントディレクトリ(カレントフォルダ)を変更するために以下のコマンドを入力します。

$ cd /Users/murayama/Desktop/php-basic

ターミナル上で現在作業している(開いている)ディレクトリをカレントディレクトリと呼びます。cdコマンドによってカレントディレクトリを変更することができます。ここではカレントディレクトリを/Users/murayama/Desktop/php-basicに変更しています。

注意:自分のMacに合わせて上記のパス/Users/murayama/Desktop/php-basicは置き換えてください。

それでは作成したプログラムを実行してみましょう。ターミナル上でphpコマンドを入力します。

$ php sample1.php
Hello PHP

上記のようにHello PHPと出力されればOKです。

PHPプログラムの終端となる閉じタグ?>は省略可能です。以降は?>を省略してコードを記載します。

2 変数(sample2.php

続いて変数を扱うプログラムについて見てみましょう。変数とはプログラム上でデータを扱う仕組みです。次のプログラムをsample2.phpという名前で保存します。

<?php
$name = "Andy";
echo "Hello ";
echo $name;

ここでは変数$nameを定義しています。また変数$nameの中に"Andy"という文字列データを代入しています。PHPの文字列データは""(ダブルクォーテーション)あるいは''(シングルクォーテーション)で囲む必要があります。変数に格納したデータはecho命令で出力できます。

PHPの変数は先頭に$マークが付きます。

それでは作成したプログラムを実行してみましょう。ターミナル上でphpコマンドを入力します。

$ php sample2.php
Hello Andy

上記のようにHello Andyと出力されればOKです。

さて、もう一つ変数を扱うサンプルプログラムを考えてみましょう。先ほどのプログラムはHello Andyと出力しましたが、もう一人、登場人物として"Betty"を追加してみましょう。先ほどのプログラムsample2.phpを修正します。

<?php
$name = "Andy";
$name2 = "Betty";

echo "Hello ";
echo $name;
echo "Hello ";
echo $name2;

変数が$name$name2の2つになりました。変数$nameには"Andy"$name2には"Betty"がそれぞれ代入されています。

それでは作成したプログラムを実行してみましょう。ターミナル上でphpコマンドを入力します。

$ php sample2.php
Hello AndyHello Betty

上記のように画面にHello AndyHello Bettyと出力されればOKです。ところでもう一人名前の出力を追加する場合はどうでしょうか。"Andy""Betty""Carol"のように名前を追加していくと、変数の数も多くなってしまいます。

3 配列(添字配列)(sample3.php

続いて配列を扱うプログラムについて見てみましょう。配列は変数の一種で、関係性のある複数のデータをまとめて管理する仕組みです。たとえば先ほどの"Andy""Betty""Carol"のような名前を表すデータは配列で管理すると簡単になります。次のプログラムをsample3.phpという名前で保存します。

<?php
$names = ["Andy", "Betty", "Carol"];

echo "Hello ";
echo $names[0];
echo "Hello ";
echo $names[1];
echo "Hello ";
echo $names[2];

変数$namesには"Andy""Betty""Carol"と3つのデータが代入されています。このように複数のデータをまとめて管理する仕組みを配列(添字配列)と呼びます。配列は前から順番に要素番号が割り振られます。また要素番号の先頭は1ではなく0から始まる点に注意しておきましょう。

変数名が$namesと複数形になっている点も注目してください。配列のような複数のデータを表現する変数は名前の付け方を工夫すると読みやすくなります。

それでは作成したプログラムを実行してみましょう。ターミナル上でphpコマンドを入力します。

$ php sample3.php
Hello AndyHello BettyHello Carol

上記のように出力されればOKです。

4 制御構文(for文)(sample4.php

配列のような集合データはfor文などの繰り返し構造を使えば簡単に出力できます。次のプログラムをsample4.phpという名前保存します。

<?php
$names = ["Andy", "Betty", "Carol"];

for ($i = 0; $i < 3; $i++) {
    echo "Hello ";
    echo $names[$i];
}

ここで繰り返し構造であるfor文は変数$iの値が0から3まで(計3回)処理を繰り返します。

for文で扱う変数$iはカウンター変数などと呼ばれます。変数$iのiはincrement(増える)という単語の頭文字を意味しています。for ($i = 0; $i < 3; $i++)とすると、変数$iの初期値は0となり、$i < 3の条件が成立する間、処理(forの後の{})を繰り返します。繰り返しが1回終了するごとに$i++が実行されます。$i++$iの値を1増やすという処理なので、繰り返しの都度、変数$iの値が1増えることになります。

それでは作成したプログラムを実行してみましょう。ターミナル上でphpコマンドを入力します。

$ php sample4.php
Hello AndyHello BettyHello Carol

上記のように出力されればOKです。

5 制御構文(if文)(sample5.php

さきほどのプログラムを少し修正してみましょう。ここではif文を使って"Andy""Betty""Carol" 3人の名前の中から"Andy"以外の名前を出力するように修正してみましょう。次のプログラムをsample5.phpという名前保存します。

<?php
$names = ["Andy", "Betty", "Carol"];

for ($i = 0; $i < 3; $i++) {
    if ($names[$i] != "Andy") {
        echo "Hello ";
        echo $names[$i];
    }
}

for文の中で、if文を使って変数($names[$i])の値が"Andy"でないか確認しています。

if文で利用している演算子!=は左辺と右辺が等しくない場合に真(True)となります。等しいかどうかを比較する場合は==を使います。!=====といった演算子も大切ですが、もう少しあとで勉強しましょう。

それでは作成したプログラムを実行してみましょう。ターミナル上でphpコマンドを入力します。

$ php sample5.php
Hello BettyHello Carol

上記のように出力されればOKです。

6 配列(連想配列)(sample6.php

少し複雑なプログラムも見てみましょう。ここでは"Andy""Betty""Carol" 3人のデータに対して"Andy"20歳、"Betty"19歳、"Carol"21歳のように、年齢(age)データも定義します。また出力の条件も変更して、年齢が20歳以上であればHelloと出力するように修正してみましょう。次のプログラムをsample6.phpという名前保存します。

<?php
$students = [
  ["name" => "Andy", "age" => 20],
  ["name" => "Betty", "age" => 19],
  ["name" => "Carol", "age" => 21]
];

for ($i = 0; $i < 3; $i++) {
    if ($students[$i]["age"] >= 20) {
        echo "Hello ";
        echo $students[$i]["name"];
    }
}

連想配列はダブルアロー演算子=>を使って定義します。通常の配列(添字配列)は前から順番に要素番号が割り振られるのに対して、連想配列は要素番号ではなく、キー("name""age"のような文字列)を割り振ります。

またfor文の中で、if文を使って変数$names[$i]["age"]の値が20以上かどうか確認しています。

それでは作成したプログラムを実行してみましょう。ターミナル上でphpコマンドを入力します。

$ php sample6.php
Hello AndyHello Carol

上記のように出力されればOKです。

7 関数(sample7.php

続いて本講座では関数について取り上げます。関数を使うサンプルプログラムを見てみましょう。次のプログラムはstrtoupper関数を使って"Andy""Betty""Carol"3人の名前をアルファベット大文字で出力します。次のプログラムをsample7.phpという名前保存します。

<?php
$names = ["Andy", "Betty", "Carol"];

for ($i = 0; $i < 3; $i++) {
    echo "Hello ";
    echo strtoupper($names[$i]);
}

for文の繰り返し処理の中でstrtoupper関数を利用しています。strtoupper関数は引数$names[$i]に受け取った値を大文字に変換する関数です。

引数とは関数の受け取るデータのことです。また関数の返却するデータは戻り値(返り値)などと呼びます。

それでは作成したプログラムを実行してみましょう。ターミナル上でphpコマンドを入力します。

$ php sample7.php
Hello ANDYHello BETTYHello CAROL

上記のように出力されればOKです。

アルファベット小文字に変換するにはstrtolower関数を使います。

PHPにはstrtoupper以外にも様々な関数が用意されています。次のサンプルプログラムはmkdir関数を使ってディレクトリ(フォルダ)を作成するプログラムです。さきほどのプログラムsample7.phpを次のように修正してみましょう。

<?php
$names = ["Andy", "Betty", "Carol"];

for ($i = 0; $i < 3; $i++) {
    mkdir($names[$i]);
}

for文の繰り返し処理の中でmkdir関数を利用しています。mkdir関数は引数$names[$i]に受け取った値を使ってディレクトリを作成します。

それでは作成したプログラムを実行してみましょう。ターミナル上でphpコマンドを入力します。

$ php sample7.php

このプログラムは実行結果をターミナルに表示しませんが、Finderやlsコマンド(Windowsの場合はExplorerやdirコマンド)で生成されたディレクトリを確認できるでしょう。

$ ls
Andy    Betty    Carol

上記のように生成されたディレクトリを確認できればOKです。

8 まとめ(sample8.php

それではこれまでのまとめとして、配列(連想配列)や制御構造(for文やif文)、それから関数(strtoupper)を使うプログラムを作成してみましょう。次のプログラムをsample8.phpという名前保存します。

<?php
$students = [
  ["name" => "Andy", "age" => 20],
  ["name" => "Betty", "age" => 19],
  ["name" => "Carol", "age" => 21]
];

for ($i = 0; $i < 3; $i++) {
    if ($students[$i]["age"] >= 20) {
        echo "Hello ";
        echo strtoupper($students[$i]["name"]);
    }
}

それでは作成したプログラムを実行してみましょう。ターミナル上でphpコマンドを入力します。

$ php sample8.php
Hello ANDYHello CAROL

上記のように出力されればOKです。

おわりに

以上、駆け足でPHPプログラミングについて紹介しました。

  1. データの出力
  2. 変数
  3. 配列(添字配列)
  4. 制御構文(for文)
  5. 制御構文(if文)
  6. 配列(連想配列
  7. 関数

わずかなプログラムでしたが、これだけでもプログラミングの基礎的な概念は網羅できています。ここで紹介したコードの読み書きができればPHPerとしての道は開けると思います。

*1:ターミナルはSpotlightでTerminalと入力すると起動します。

*2:ブラウザでPHPプログラムを開発するには https://repl.it/ などが便利です。repl.itで実行する場合はPHPの閉じタグ?>が要るみたいなので注意してください。

30minくらいで学ぶVue.jsとVuex

カウンターアプリケーションの開発を通じてVue.jsによるプログラミングとVuexによる状態管理を学びます。

f:id:yamasahi:20190213141451p:plain

ボタンを押したら数字が増えていくアプリケーションです。

Agenda

  • Part 1 Vueアプリケーションの開発(10min)
  • Part 2 Vuexを活用したVueアプリケーションの開発(10min)
  • Part 3 非同期処理(Actions)の実装(10min)

Part 1 Vueアプリケーションの開発(10min)

プロジェクトの作成

vue-cliを使ってプロジェクトの雛形を作成します。

vue create counter-app

vue-cliの設定はデフォルトを使います。途中でVuexを追加しますが手動で追加するものとします。

プロジェクトが作成できたらディレクトリを移動してサーバを起動してみましょう。ここではyarnを使って起動しています(インストールの設定によってはnpmでも大丈夫です)。

cd counter-app
yarn serve

次のような画面を確認できればOKです。

f:id:yamasahi:20190213141744p:plain

Vueコンポーネント(MyCounter.vue)の作成

vue-cliで作成したプロジェクトの開発では主にsrcディレクトリ下のファイルを編集することになります。

  • src/
    • assets/
      • 画像ファイルなどのアセット
    • components/
      • App.vueからロードされる画面の部品となるVueファイル
    • App.vue
      • main.jsからロードされるVueファイル
    • main.js
      • 起動ファイル

src/components/Mycounter.vueファイルを新規作成します。

<template>
    <div>
        Count: {{ count }}
        <button @click="onclick">Increment</button>
    </div>
</template>

<script>
export default {
    data () {
        return {
            count: 1
        }
    },
    methods: {
        onclick () {
            this.count++
        }
    }
}
</script>

続いてsrc/App.vueを修正します。

<template>
  <div id="app">
    <img alt="Vue logo" src="./assets/logo.png">
-    <HelloWorld msg="Welcome to Your Vue.js App"/>
+    <MyCounter />
  </div>
</template>

<script>
-import HelloWorld from './components/HelloWorld.vue'
+import MyCounter from '@/components/MyCounter.vue'

export default {
  name: 'app',
  components: {
-    HelloWorld
+    MyCounter
  }
}
</script>

<style>
#app {
  font-family: 'Avenir', Helvetica, Arial, sans-serif;
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
  text-align: center;
  color: #2c3e50;
  margin-top: 60px;
}
</style>

ブラウザでサーバ(localhost:8080)にアクセスすると次のような画面が表示されるでしょう。

Part 2 Vuexを活用したVueアプリケーションの開発(10min)

Vuexのインストール

つづいてVuexをインストールします。

VuexはVue.jsアプリケーションのための状態管理パターン+ライブラリです。 https://vuex.vuejs.org/ja/

$ yarn add vuex --save

ここではMyComponent.vueの状態(dataプロパティ)をVuex上で管理するように修正していきます。

Storeの作成

Vuex上で状態を管理するためのStoreファイルを作成します。src/store/index.jsを作成し、以下のように実装します。

import Vue from 'vue'
import Vuex from 'vuex'

Vue.use(Vuex)

const store = new Vuex.Store({
    state: {
        count: 0
    },
    mutations: {
        increment (state) {
            state.count++
        },
    },
})

export default store

VuexのStoreには以下の主に4つを記述します。

  • state・・・状態データ
  • mutations・・・状態を更新する処理
  • getters・・・状態を取得する処理
  • actions・・・非同期処理(mutationsを呼び出す)

gettersとactionsについては後述します。

Vueコンポーネント(MyCounter.vue)の修正

src/components/MyCounter.vueを次のように修正します。

 <template>
     <div>
         Count: {{ count }}
         <button @click="onclick">Increment</button>
     </div>
 </template>

 <script>
 export default {
-    data () {
-        return {
-            count: 1
-        }
-    },
+    computed: {
+        count() {
+            return this.$store.state.count;
+        }
+    },
     methods: {
         onclick () {
-            this.count++
+            this.$store.commit('increment')
         }
     }
 }
 </script>

VueコンポーネントからStoreにアクセスするにはthis.$storeと記述します。this.$store.state.countで状態(state)にアクセスし、状態を更新するmutationsを実行するにはcommitメソッドを使います。

ブラウザでサーバ(localhost:8080)にアクセスして正しく動作することを確認しておいてください。

mapGetters、mapMutationsによるリファクタリング

Vuexには以下のヘルパーメソッドが用意されています。

  • mapState
  • mapMutations
  • mapGetters
  • mapActions

上記のヘルパーメソッドを使うとVueコンポーネントの記述がシンプルになります。

ここではmapGettersとmapMutationsを利用するように修正してみます。

まずはsrc/store/index.jsを修正します。Storeファイルにgettersを追加します。

 import Vue from 'vue'
 import Vuex from 'vuex'

 Vue.use(Vuex)

 const store = new Vuex.Store({
     state: {
         count: 0
     },
+    getters: {
+        count: state => state.count
+    },
     mutations: {
         increment (state) {
             state.count++
         },
     },
 })

 export default store

ここでgettersは単純にstateを返却していますが、gettersはStoreの状態を算出(フィルタリングなど)する用途に使えます。

次にsrc/componetns/MyCounter.vueを修正します。

<template>
    <div>
        Count: {{ count }}
-        <button @click="onclick">Increment</button>
+        <button @click="increment">Increment</button>
    </div>
</template>

<script>
+import { mapGetters, mapMutations } from 'vuex'
+
export default {
    computed: {
-        count() {
-            return this.$store.state.count;
-        }
+        ...mapGetters(['count'])
    },
    methods: {
-        onclick () {
-            this.$store.commit('increment')
-        }
+        ...mapMutations(['increment'])
    }
}
</script>

mapMutationsやmapGettersは戻り値にオブジェクトを返すので、オブジェクトスプレッド演算子を合わせて利用します。また引数には利用したい処理を定数の配列で渡すようにします。

ブラウザでサーバ(localhost:8080)にアクセスして正しく動作することを確認しておいてください。

Part 3 非同期処理(Actions)の実装(10min)

Storeの修正

続いて非同期処理を実装してみます。ここではボタンを押してから2秒後にインクリメントするIncrement Asyncボタンを実装します。

まずはsrc/store/index.jsを修正します。

 import Vue from 'vue'
 import Vuex from 'vuex'

 Vue.use(Vuex)

 const store = new Vuex.Store({
     state: {
         count: 0
     },
     getters: {
         count: state => state.count
     },
     mutations: {
         increment (state) {
             state.count++
         },
     },
+    actions: {
+        incrementAsync ({ commit }) {
+            setTimeout(() => commit('increment'), 2000)
+        }
+    }
 })

 export default store

新たにactionsを定義しています。actionsではmutationsをcommitするようにします。

actionsはWeb APIの呼び出しなどに適しています。

Vueコンポーネント(MyCounter.vue)の修正

続いてsrc/components/MyCounter.vueを修正します。

<template>
    <div>
        Count: {{ count }}
        <button @click="increment">Increment</button>
+        <button @click="incrementAsync">Increment Async</button>
    </div>
</template>

<script>
import { mapGetters, mapMutations } from 'vuex'

export default {
    computed: {
        ...mapGetters(['count'])
    },
    methods: {
+        incrementAsync () {
+            this.$store.dispatch({
+                type: 'incrementAsync'
+            })
+        },
        ...mapMutations(['increment'])
    }
}
</script>

新たにincrementAsyncメソッドを追加しています。incrementAsyncメソッドではStoreに対してdispachメソッドを呼ぶことでactionsを呼び出しています。

ブラウザでサーバ(localhost:8080)にアクセスして正しく動作することを確認しておいてください。

mapActionsによるリファクタリング

続いてmapActionsを使ってリファクタリングしてみましょう。src/components/MyCounter.vueを修正します。

<template>
    <div>
        Count: {{ count }}
        <button @click="increment">Increment</button>
        <button @click="incrementAsync">Increment Async</button>
    </div>
</template>

<script>
-import { mapGetters, mapMutations } from 'vuex'
+import { mapGetters, mapMutations, mapActions } from 'vuex'

export default {
    computed: {
        ...mapGetters(['count'])
    },
    methods: {
-        incrementAsync () {
-            this.$store.dispatch({
-                type: 'incrementAsync'
-            })
-        },
+        ...mapActions(['incrementAsync']),
        ...mapMutations(['increment'])
    }
}
</script>

ブラウザでサーバ(localhost:8080)にアクセスして正しく動作することを確認しておいてください。

まとめ

Vuexを使ってVue.jsアプリケーションの状態管理を試してみました。Vuexの詳細については公式のマニュアルを読めば大体わかります。

https://vuex.vuejs.org/ja/

以降はVue Routerを理解して、Nuxt.jsの学習を進めていくと良いでしょう。

勢いでMac買っちゃった人のためのWebプログラミング開発環境構築

同級生(昭和54年生まれ)の友達が最近プログラミングに興味を持っていて「Mac買おうかなー」と迷っていたのでまとめました。プログラミング楽しいよ。

私とプログラミング

2019年1月からMacBook Pro(15)に乗り換えました。以前の13インチに比べて5cmくらい?画面が大きくなり、しばらく満足していたのですが1週間も経つともっと画面広かったらなーと思うようになりました。しばらくコワーキングで仕事しているのでデュアルディスプレイでないのが少しさびしい。

これからはしばらくFirebaseを中心にWebアプリケーションをいくつか作る予定です。フロントエンドはVue.jsを中心にJavaScriptをしっかりやろうと思っています。あとたまにPHPのコードも書きます。そんなわけで必要なツールはエディターとターミナルとブラウザがあれば十分です。というわけで以下のアプリをインストールしました。

エディタ(VS Code

エディタはこれまでAtomを使っていましたが(今も使っていますが)VS Codeに乗り換えました。

code.visualstudio.com

デフォルトでも機能が十分ですがVimプラグインなどを入れてプログラマーっぽくしています。他にはGitLensとかGit系の拡張を入れたり、FirebaseとかVueとかのJSまわりのプラグインをいくつかインストールしました。

WakaTime

VS Codeにはプログラミングの時間を計測してくれるWakaTimeプラグインもインストールしてみました。

wakatime.com

ちなみに私の最近のプログラミング時間はこんなかんじ。

https://wakatime.com/@murayama333

DAILY AVERAGE 2 hrs 56 mins

最近だと毎日3Hくらいコーディングの時間を確保できているみたい。5Hくらいいきたい。これからプログラミング始める人にもおすすめのプラグインです。

Dash

JavaScriptとかAPIよくわかっていないのでDash入れてみました。

kapeli.com

ショートカットキーが安定していないけど、cmd + shift + spaceとかでAPIをひけるようにしています。あとスニペット機能も優秀みたいだけどまだあまり試していない。

ターミナル(iTerm2)

続いてターミナル編。デフォルトのターミナルでも良いような気がしますがプログラマーっぽくありたいのでiTerm2をインストールしました。

www.iterm2.com

CUIベースでコード書くのもカッコいいのでVimでいくべきか迷うのですが、Vim素人なので今はVS Codeでコードを書くようにしています。そうするとエディタとiTerm2の切り替えが頻繁に発生するのでiTerm2のホットキー設定を有効にしています。Alt + SpaceでいつでもiTtem2が表示されるので気持ち良いです。ctrlキー連打で開くのもありみたいです。

blog.mah-lab.com

Homebrew

Macだとお約束のパッケージ管理ツールHomebrewをインストールします。

brew.sh

インストールは上記のページを参考に。インストールが完了するとbrewコマンドでパッケージを追加できるようになります。以下はwgetコマンドをインストールする例です。

$ brew install wget

zsh

デフォルトのシェルはzshに変更しています。複数のターミナル間でコマンドのヒストリを共有できるのが有り難いです。たしかzshbrewでインストールしたような気がします。

$ brew install zsh

tmux

iTermの中ではtmuxをインストールしています。

$ brew install tmux

tmuxを使えば、iTermの画面表示を分割したりタブ移動したり便利です。プレフィックスキーはこれまでctrl + tを使っていましたが、今回からはctrl + spaceに変更しました。余談ですがVimのleaderもspaceキーに変更していてspace駆動で操作できるように設定しています。

以下のサイトを参考にしています。

qiita.com

qiita.com

tmuxのステータスバーにバッテリーとかWi-Fiとかいろんな情報を表示すると楽しいです。

peco

シェルではctrl + rでコマンドの履歴を検索したいのでpecoを入れておきました。brewでインストールできます。

$ brew install peco

以下のサイトを参考にしています。

qiita.com

ctrl + x でcdコマンドを補完できるのが地味に便利です。

Vim

やっぱりVimの設定も楽しいです。LIGさんの設定がシンプルで良かったです。

liginc.co.jp

上記のページでも紹介がありましたが、少し古い記事ですが「Vimの生産性を高める12の方法」も面白かったです。

postd.cc

ブラウザ(Chrome

とりあえずChromeを入れました。あんまりカスタマイズはしていないです。はてなブックマーク拡張は入れています。あとはVueの開発用の拡張とか。

ユーティリティ系

ウィンドウのリサイズにはSpectacleがちょうどいいです。

www.spectacleapp.com

あとはVS Codeでctrl + m での改行が上手く効いてくれないのでキーのリマップもやろうか迷い中。Karabinerのお世話になるかも。

pqrs.org

画面を広く使いたい

ここまでベタなアプリの設定ばかりだったので。ライフハック?的なやつも。

これからしばらくMac1台で仕事をするので画面を広く効率的に使いたいところです。なので、まずMacの「システム環境設定」で以下のとおり変更します。

  • Dockを隠す
  • メニューバーを隠す

そうすると画面が少し広くなります。

次にiTerm2からタイトルバーを消します。iTerm2の環境設定Profile=>WindowにあるStyleで No Title Barを選びます。

f:id:yamasahi:20190207124258p:plain

タイトルバーが消えて1cmくらいだけでも画面が広くなると嬉しいです。

調べてみたらVS CodeAtomもタイトルバーをプラグインで消すことができました。

marketplace.visualstudio.com

VS Codeの拡張は再起動時に警告が出るけどとりあえず無視しています。

以上でこんなかんじになります。

f:id:yamasahi:20190207124348p:plain

ちなみに画面を広く使って集中していると「今何時?」となることが多いので、ターミナルとかエディタのステータスバーに時刻を表示するようにしています。

フリーランス始めました。

近況です。元気にやっています。

  • 税務署に行った。開業届(青色申告)を提出した。
  • 税務署はこわいイメージがあったけど書類を出すのは1分で終わった。
  • フリーランス始めました。
  • MacBook Proを買った。
  • 大きいサイズ(15インチ)にしたら快適だった。
  • お名前.comでドメインを取った。
  • お名前.comからの案内メールが多すぎ。
  • GSuiteを申し込んだ。
  • https://gsuite.google.co.jp/intl/ja/
  • Googleあればどこでも仕事できそうな気がした。
  • Freeeを申し込んだ。
  • https://www.freee.co.jp/
  • 会計処理だけでなく見積もり書とか、書類関連も作ってくれた。
  • Freeeがあればぼくでも仕事ができそうな気がした。
  • 印鑑も買った。
  • ほとんどWebやし印鑑とか意味あるのかなーと思いつつも購入した。
  • ビジネスにおけるネクタイみたいなもの、と聞いて納得。
  • ちなみに印鑑屋さんは減ってるらしい。
  • 名刺はまだない。
  • コワーキングに通い出した。
  • 月額1万円くらい。ほどよく空いている。
  • 家からコワーキングまで片道3kmを歩いてる。
  • まだ痩せていない。
  • むしろ肥えた。
  • フリーランスの税金について勉強をした。
  • 雰囲気わかってきた。
  • この本のおかげ
  • https://www.amazon.co.jp/dp/4801400604
  • 口座も開設した。
  • デビットカード
  • 領収書(レシート)をもらうようになった。
  • 案の定、財布がパンパンになった。
  • Windowsマシンも必要だと気づいてThinkPadを買った。
  • MacよりThinkPadで仕事している人の方がかっこいい説も浮上。
  • スマホの充電ができていないことが多い。
  • スマホも買い換えようか迷う。
  • なんでも経費に見える。
  • 平日の昼間に映画:ボヘミアン・ラプソディを観た。
  • この10年で観た映画の中で1番良かった。
  • フレディ・マーキュリーのファンに。
  • Spotifyは経費扱いになるのだろうか。
  • ちなみに映画を見たのは10年ぶり。
  • 学校を訪問させて頂いた。
  • 勉強する空間は良い。
  • 年齢とか関係なくいろいろ学び直したい。
  • お世話になった人に再会できた。
  • ヒントも頂いた。
  • これからもお世話になります。
  • 前の職場を訪問した。
  • みんな元気そうだった。
  • いうても2週間ぶり。
  • 今はまだ懐かしさとかない。
  • Webで知り合った社会人の方にプログラミングを教えている。
  • Webで知り合った大学生にもプログラミングを教えている。
  • お二人ともリピートしてくれている。
  • ありがたい。
  • 動画教材づくりにUdemyを始めようかと思っている。
  • 教えてもらったCamtasiaをインストールして動画づくりを始めている。
  • テキスト教材づくりも今までのやり方を見直している。
  • MarkdownよりAsciidocが良いみたい。
  • https://qiita.com/xmeta/items/de667a8b8a0f982e123a
  • 電子書籍づくりをGitHubで管理している人もいる。
  • https://azu.github.io/slide/individual/
  • すごく参考になる。
  • Firebaseでアプリを作り始めた。
  • GoogleのCodelabsのチュートリアルがすごく充実している。
  • https://codelabs.developers.google.com/
  • 今は作る時間を最優先にしたい。
  • とかいいつつ、こんなブログを書いている。
  • ベンチャー企業の方とお会いすることが増えた。
  • 志、技術、マーケティング。全部大事だと思う。
  • 組織づくりに興味が湧いてきた。
  • OKRの本(ジョン・ドーア本)が良かった。
  • https://www.amazon.co.jp/dp/4532322405
  • 去年読めなかったサピエンス全史も読み終えた。
  • コロンブスの新大陸発見からいろいろ繋がっているように思う。
  • 「文系でプログラマーになったけど色々失敗して3年半で会社を辞めた話」を読んだ。
  • https://note.mu/denkigai/n/nafff6bd87802
  • 「プログラミングを覚えてから何をすればいいかわからなかった」
  • と感じている人は確かに多いかも。
  • 例えとしてのSTRIDERの話も面白かった。
  • 最近noteって流行ってるように思う。
  • 飛行機に乗った。
  • 1時間のフライトも想像力が邪魔をして生きた心地がしなかった。
  • 登壇した。
  • 仕事している感じがした。
  • パフォーマーだ」
  • ブログも書いた。

JavaScript中級トレーニング10問(String編)

この前の記事(JavaScript 30-seconds-of-code String編)をベースにJavaScript問題集を作ってみました。

murayama.hatenablog.com

問題は全部で10問。正規表現をゴリゴリ使うのはなるべく避けてみました。個人的にはヒント見ずに(ググらずに)1時間で全部解けたらマスタークラスだと思います。

  1. isLowerCase
  2. reverseString
  3. truncateString
  4. mapString
  5. mask
  6. compactWhitespace
  7. palindrome
  8. capitalize
  9. byteSize
  10. pluralize

開発環境はブラウザとエディタで十分ですがCodeSandboxも便利です。

codesandbox.io

CodeSandboxをVanillaで起動してもらうとJavaScriptコードの勉強にちょうど良いです。

f:id:yamasahi:20190116085210p:plain

それでははりきってどうぞ。


1 isLowerCase

文字列が小文字か検証します。次の実行結果となるようにisLowerCase関数を定義してください。

isLowerCase('abc'); // true
isLowerCase('a3@$'); // true
isLowerCase('Ab4'); // false

isLowerCase関数を実装して、console.log(isLowerCase('abc')) でtrueが出力されればOKです。

ヒント

String.prototype.toLowerCase()を使って、与えられた文字列を小文字に変換し、元の文字列と等しいか比較します。

const isLowerCase = str => str === str.t__________();

2 reverseString

文字列を反転します。次の実行結果となるようにreverseString関数を定義してください。

reverseString('foobar'); // 'raboof'

ヒント スプレッドオペレータ(...)とArray.prototype.reverse()を使って文字列内の文字の並びを逆順にしString.prototype.join('')によって文字を連結して文字列とします。

const reverseString = str => [...str].r______().j___('');

3 truncateString

指定したサイズで文字列を切り詰めます。次の実行結果となるようにtruncateString関数を定義してください。

truncateString('boomerang', 7); // 'boom...'

ヒント 文字列のサイズの上限を指定します。切り詰められた文字列の後部に'...'を連結して返します。

const truncateString = (str, num) =>
  str.l_____ > num ? str.s____(0, num > 3 ? num - 3 : num) + '...' : str;

4 mapString

文字列に含まれる個々の文字に対して、指定されたコールバック関数を適用して、新たな文字列を生成します。次の実行結果となるようにmapString関数を定義してください。

mapString('lorem ipsum', c => c.toUpperCase()); // 'LOREM IPSUM'

ヒント String.prototype.split('')とArray.prototype.map()を使って、コールバック関数(引数のfn)を文字列内の個々の文字に対して適用します。それからArray.prototype.join('')を使って文字配列を文字列として再結合します。コールバック関数は3つの引数(現在の文字、現在の文字のインデックス、mapString関数の引数に指定された文字列)を受け取ります。

const mapString = (str, fn) =>
  str
    .s____('')
    .m__((c, i) => f_(c, i, str))
    .j___('');

5 mask

文字列を指定されたマスク文字で置き換えます。ただし、文字列の後部については、引数のnumに指定された文字数分はマスクしません。次の実行結果となるようにmask関数を定義してください。

mask(1234567890); // '******7890'
mask(1234567890, 3); // '*******890'
mask(1234567890, -4, '$'); // '$$$$567890'

ヒント String.prototype.slice()を使って、アンマスク(マスクしない)する文字列を取り出し、String.prototype.padStart()で、元の文字列の長さ分のマスク文字を追加します。第2引数のnumが省略された場合、デフォルトで4文字のアンマスク文字を確保します。またnumに負の値が指定された場合は、文字列の先頭部分をアンマスクします。第3引数のmaskが省略された場合、デフォルトのマスク文字に*を使います。

const mask = (cc, num = 4, mask = '*') => `${cc}`.s____(-num).p_______(`${cc}`.l_____, mask);

6 compactWhitespace

連続するホワイトスペース文字(スペース、タブ、改ページ、改行)をホワイトスペース文字1文字に置き換えます。次の実行結果となるようにcompactWhitespace関数を定義してください。

compactWhitespace('Lorem    Ipsum'); // 'Lorem Ipsum'
compactWhitespace('Lorem \n Ipsum'); // 'Lorem Ipsum'

ヒント 正規表現とString.prototype.replace()を使うことで、2文字以上のホワイトスペース文字を1文字に置き換えます。

const compactWhitespace = str => str.r______(/\s{2,}/g, ' ');

7 palindrome

与えられた文字列が回文になっているか検証します。回文の場合、true、そうでない場合、falseを返します。次の実行結果となるようにpalindrome関数を定義してください。

palindrome('taco cat'); // true

ヒント String.prototype.toLowerCase()とString.prototype.replace()を使って非アルファベット文字を除去して小文字に統一し、それからスプレッドオペレータ(...)を使って文字列を文字の配列に変換し、Array.prototype.reverse()、String.prototype.join('')を使って生成した文字列と、元の文字列(非アルファベット文字を除去して小文字に統一したもの)を比較します。

const palindrome = str => {
  const s = str.t__________().r______(/[\W_]/g, '');
  return s === [...s].r______().j___('');
};

8 capitalize

文字列の先頭文字を大文字に変換します。次の実行結果となるようにcapitalize関数を定義してください。

capitalize('fooBar'); // 'FooBar'
capitalize('fooBar', true); // 'Foobar'

ヒント 配列の分割代入とString.prototype.toUpperCase()を使って先頭の文字を大文字にします。 ...restには先頭文字を除く文字の配列が格納されるのでArray.prototype.join('')を使って再び文字列に復元しています。引数のlowerRestが指定されなかった場合は残りの文字列(先頭文字を除く)をそのまま使います。lowerRestにtrueが指定された場合は残りの文字列を小文字に置き換えます。

const capitalize = ([first, ...rest], lowerRest = false) =>
  first.t__________() + (lowerRest ? rest.j___('').t__________() : rest.j___(''));

9 byteSize

文字列の長さをbytesで返します。次の実行結果となるようにbyteSize関数を定義してください。

byteSize('😀'); // 4
byteSize('Hello World'); // 11

ヒント 文字列をBlobオブジェクトに変換してsizeプロパティを参照します。

const byteSize = str => new B___([str]).s___;

10 pluralize

入力された数値によって、基準となる文字列を単数系、あるいは複数形にして返します。第1引数にオブジェクトが指定された場合、関数によって返却されるクロージャを返します。これは"s"による単純な複数形の変換だけでなく、指定されたディクショナリに含まれる複数形単語を返却します。次の実行結果となるようにpluralize関数を定義してください。

pluralize(0, 'apple'); // 'apples'
pluralize(1, 'apple'); // 'apple'
pluralize(2, 'apple'); // 'apples'
pluralize(2, 'person', 'people'); // 'people'

const PLURALS = {
  person: 'people',
  radius: 'radii'
};
const autoPluralize = pluralize(PLURALS);
autoPluralize(2, 'person'); // 'people'

ヒント numが-1か1の場合、単数系の単語を返却します。numがそれ以外の値の場合、複数形の単語を返却します。第3引数のpluralが省略された場合、デフォルトで第2引数のwordに"s"を連結した文字列を複数形の単語として処理するので、必要に応じてカスタマイズすることができます。第1引数のvalがオブジェクトの場合、複数形の単語を格納したディクショナリを保持したクロージャを返却します。

const pluralize = (val, word, plural = word + 's') => {
  const _pluralize = (num, word, plural = word + 's') =>
    [1, -1].i_______(Number(num)) ? word : plural;
  if (typeof val === 'object') return (num, word) => __________(num, word, val[word]);
  return __________(val, word, plural);
};

答え

お疲れ様でした。答えはこちらの記事を参考に。

murayama.hatenablog.com