LINE Flex Messageを検討してみた

エニグモの @takurokamiyoshi です。 この記事は Enigmo Advent Calendar 2018 の21日目の記事です。

私は主にフロントエンド周りの実装やディレクション業務を行っています。 弊社ではファッションECサイトであるBUYMAを運営しており、 利用者とのコミュニケーションツールとしてメールやアプリ、そしてLINEも重要なツールだと考えています。

BUYMA LINEアカウントについて

https://www.buyma.com/contents/line/ これからLINEアカウントの友だち数も増やして、LINEアカウント内でいろんな施策を実施したいと思っています。

LINE Messaging APIとは?

LINE社が提供するLINEカウントを通じて、ユーザとの双方向コミュニケーションを実現するAPIです。 メール施策で実施しているシナリオ配信のような配信に利用にしたり、ボットとして活用しているLINEアカウントも多いようです。

※とりあえずLINE Messaging API試してみたい人は無料で試せるLINE Developer Trialプランがあります。LINEアカウントを作成いただいてcurlでLINE Messaging APIをコールしてもらえればと思います。

Flex Messageとは?

LINE Messaging APIではテキスト、画像、動画、カルーセルといった様々なメッセージタイプでメッセージを配信することができます。 Flex Messageは複数の要素を組み合わせてレイアウトを自由にカスタマイズできるメッセージです。 html、cssに近い感覚でメッセージレイアウトを考えることができます。

Simulator

Flex Message Simulator LINE社が提供しているものになります。 Simulatorなしでゼロから作成するのはかなり困難で Flex Message Simulatorではサンプルがいくつか準備されているものでこれをベースに考えると良いと思います。

Flex Messageはレイアウトの自由度が高いのでいろいろ検討が必要だと思い今回の記事を作成しようと思いました。

レイアウト案

いくつかの案を記載していきます。 ※curlの{XXX} の部分は各環境で置き換えてください。 ※curlは長くなったので一部改行、空白なしにしています。

縦積みパターン

とりあえず何かに使えそうだと思います。

curl -v -X POST https://api.line.me/v2/bot/message/push \
-H 'Content-Type:application/json' \
-H 'Authorization: Bearer {channel access token}' \
-d '{
    "to": "{userId}",
    "messages":[
        {  
          "type": "flex",
          "altText": "this is a flex message",
          "contents": {"type":"bubble","body":{"type":"box","layout":"vertical","spacing":"none","margin":"none","contents":[{"type":"image","url":"{url}","aspectMode":"cover","aspectRatio":"5:2","size":"5xl","action":{"type":"uri","uri":"{uri}"}},{"type":"image","url":"{url}","aspectMode":"cover","aspectRatio":"5:2","margin":"sm","size":"5xl","action":{"type":"uri","uri":"{uri}"}},{"type":"image","url":"{url}","aspectMode":"cover","aspectRatio":"5:2","margin":"sm","size":"5xl","action":{"type":"uri","uri":"{uri}"}},{"type":"image","url":"{url}","aspectMode":"cover","aspectRatio":"5:2","margin":"sm","size":"5xl","action":{"type":"uri","uri":"{uri}"}},{"type":"image","url":"{url}","aspectMode":"cover","aspectRatio":"5:2","margin":"sm","size":"5xl","action":{"type":"uri","uri":"{uri}"}}]}}
        }
    ]
}'

商品一覧パターン

スマホサイトの商品一覧のような形のレイアウトです。 もっと長くしてもよいかなとも思いました。

curl -v -X POST https://api.line.me/v2/bot/message/push \
-H 'Content-Type:application/json' \
-H 'Authorization: Bearer {channel access token}' \
-d '{
    "to": "{userId}",
    "messages":[
        {  
          "type": "flex",
          "altText": "this is a flex message",
          "contents": {"type":"bubble","hero":{"type":"image","url":"{url}","size":"full","aspectRatio":"10:3","aspectMode":"cover","action":{"type":"uri","uri":"{uri}"}},"body":{"type":"box","layout":"horizontal","spacing":"md","contents":[{"type":"box","layout":"vertical","flex":1,"contents":[{"type":"image","url":"{url}","aspectMode":"cover","aspectRatio":"1:1","size":"xxl","action":{"type":"uri","uri":"{uri}"}},{"type":"image","url":"{url}","aspectMode":"cover","aspectRatio":"1:1","margin":"md","size":"xxl","action":{"type":"uri","uri":"{uri}"}},{"type":"image","url":"{url}","aspectMode":"cover","aspectRatio":"1:1","margin":"md","size":"xxl","action":{"type":"uri","uri":"{uri}"}}]},{"type":"box","layout":"vertical","flex":1,"contents":[{"type":"image","url":"{url}","aspectMode":"cover","aspectRatio":"1:1","size":"xxl","gravity":"bottom","action":{"type":"uri","uri":"{uri}"}},{"type":"image","url":"{url}","aspectMode":"cover","aspectRatio":"1:1","margin":"md","size":"xxl","action":{"type":"uri","uri":"{uri}"}},{"type":"image","url":"{url}","aspectMode":"cover","aspectRatio":"1:1","margin":"md","size":"xxl","action":{"type":"uri","uri":"{uri}"}}]}]},"footer":{"type":"box","layout":"horizontal","contents":[{"type":"button","action":{"type":"uri","label":"More","uri":"{uri}"}}]}}
        }
    ]
}'

細いメッセージパターン

意外と需要がありそうです。 リッチメニューを出しているとメッセージが見える範囲が狭くなりますし、画像作成は必要ありません。

curl -v -X POST https://api.line.me/v2/bot/message/push \
-H 'Content-Type:application/json' \
-H 'Authorization: Bearer {channel access token}' \
-d '{
    "to": "{userId}",
    "messages":[
        {  
          "type": "flex",
          "altText": "this is a flex message",
          "contents": {"type":"bubble","body":{"type":"box","layout":"horizontal","contents":[{"type":"button","style":"primary","action":{"type":"uri","label":"リンク1","uri":"{uri}"}},{"type":"button","style":"secondary","action":{"type":"uri","label":"リンク2","uri":"{uri}"}}]}}
        }
    ]
}'

まとめ・感想

自由度が高いので細かいこだわりを反映できるメッセージですがやはり複雑なものを考えるのは難しいです。 Flex Messageをゼロから作成するのは結構大変なので、他の人が作成したものを共有していけるようにしていければ良いなと思いました。 ざっと触ってみてちょっとだけ慣れてきました。慣れてくれば、Simulatorなしでも作成できるようになるのだろうか。。 また以前はマルチキャスト /v2/bot/message/multicastFlex Messageを配信できなかったですが使用できるようになったので大量配信にも使えそうです。 今回調べたことを実際の施策の中で活かしていきたいと思います。

参考

LINE Developers ドキュメント

Gitlab CIを利用したGCP(GKE)への自動デプロイ

この記事は Enigmo Advent Calendar 201820日目の記事です。

概要

GCP(GKE)を利用してログからユーザ属性を機械学習により予測し、 その結果をAPIで返却するシステムを構築しました。 運用していく上でCI/CDツールは何にしようかなぁというところで、 今後社内でGitlabを使っていくという流れがあったのと GKEとの相性も良さそうということでGitlab CIを利用することにしました。1

今回はGitlabとGCP(GKE)環境を連携する方法と、 Gitlab CIのキモとなる.gitlab-ci.ymlの内容についてまとめたいと思います。

システム構成

簡単にデプロイ対象のシステム構成を紹介します。 赤い枠で示したものが今回の作業対象になります。

環境条件

今回は下記の環境を使用しています。 Gitlabについては毎月バージョンが更新されるので、旧バージョンを利用されている方などは若干UI等が変わっているかもしれません。

  • Gitlab
    GitLab Community Edition 11.5.3
  • GKE
    クラスタバージョン 1.10.9-gke.5

CI/CDのフロー

CI/CDは下記のフローで行います。

テスト環境

  • developブランチへのcommit&push

以降は自動

  • dockerイメージのbuild
  • dockerイメージをGCSへpush
  • GKE(CronJon)へのデプロイ
  • GKE(Deployment)へのデプロイ

本番環境

本番はすべて手動

  • developブランチからmasterブランチへのMR作成
  • マージ
  • GKE(CronJon)へのデプロイ
  • GKE(Deployment)へのデプロイ

設定手順

Gitlab CIを利用するにあたり必要な設定を行っていきます。

サービスアカウント作成

GCP(GKE)とGitlab CIとの連携のためにGCPコンソールからサービスアカウントを作成します。

  • ここでは仮に「Gitlab CI」という名前のサービスアカウントを作成します。
  • 役割(role)に下記の2つを付与してください。
    • Kubernetes Engine 開発者
    • ストレージ管理者
  • キーのタイプでJSONを選択し、キーを作成し、ローカルにキーファイルをdownloadします。

変数の設定

GKEへの認証情報をGitlabに登録します。 対象のプロジェクト(リポジトリ)のSettings -> CI/CD -> 変数にて、 キーに「SERVICE_ACCOUNT_KEY」を入力し、 値に先ほどdownloadしたキーファイルの中身をそのままコピペします。

下記の例では「SERVICE_ACCOUNT_KEY_PROD」というキーもありますが、 これは本番とテストでプロジェクトを分けているためです 1つのプロジェクト配下の場合は1つのキーで問題ないと思います。

Specific Runnersの登録

下記コマンドにてSpecific Runnerを登録します。 今回はdocker in docker方式で行います。 shell executorを利用する場合はこちらを参考にしてください。

$ sudo gitlab-runner register -n \
> --url [URL] \
> --registration-token [TOKEN] \
> --executor docker \
> --description "docker-runner" \
> --tag-list "gke" \
> --docker-image "docker:stable" \
> --docker-privileged

※指定するURLとTOKENについてはGitlabのSettings -> CI/CD -> Runnerの項目を参照してください。

今回はオンプレ版のGitlab環境を使用していますが、 gitlab.comでShared Runnerを利用する場合には こちらの手順は不要です。

.gitlab-ci.ymlの作成

今回はCronjobへのデプロイ定義を例に説明します。 Deploymentへのデプロイ定義も基本的に同じです。

  • 共通設定
    • テスト/本番環境など、デプロイ対象の環境が複数ある場合にはどうしても定義が冗長的になります。
    • それを解消するために、下記ではYAMLのアンカー/エイリアスとGitlabのextends(v11.3で追加)という機能を利用して、共通的な定義はまとめるようにしています。
.auth_gke: &auth_gke |
  gcloud auth activate-service-account --key-file=key.json
  gcloud config set project ${PRJ_ID}
  gcloud config set container/cluster ${CLUSTER_NAME}
  gcloud config set compute/zone ${CLUSTER_ZONE}
  gcloud container clusters get-credentials ${CLUSTER_NAME} --zone ${CLUSTER_ZONE}

.setting_dev:
  environment: develop
  variables:
    CI_DEBUG_TRACE: 'true'
    PRJ_ID: 'XXXXX-dev'
    CLUSTER_NAME: 'dev-XXXX'
    CLUSTER_ZONE: 'asia-northeast1-a'
    GCS_KEY: 'XXXX.json'
    ENV_CONFIG: '/PATH/conf/settings_dev.json'

.setting_prod:
  environment: production
  variables:
    PRJ_ID: 'XXXXX-prod'
    CLUSTER_NAME: 'XXXXX'
    CLUSTER_ZONE: 'asia-northeast1-a'
    GCS_KEY: 'XXXX.json'
    ENV_CONFIG: '/PATH/conf/settings_prod.json'

services:
  - docker:dind

stages:
  - build
  - deploy
  • ジョブ定義①
    ここではdockerイメージのbuildと、GCRへのコンテナイメージのpushを行っています。
    • extendsで先程定義した内容を継承しています。
    • only:changesで該当のファイルに変更があった場合のみジョブが実行されるようになります
docker build_base:
  extends: .setting_dev
  image: 'docker:stable'
  stage: build
  tags:
    - gke
  script:
    - docker info
    # Build the image
    - docker build --cache-from ${IMAGE_TAG} -t ${IMAGE_TAG}:${IMAGE_VER} .
    # Log in to Google Container Registry
    - echo "$SERVICE_ACCOUNT_KEY" > key.json
    # - docker login -u _json_key --password-stdin https://asia.gcr.io < key.json
    - docker login -u _json_key -p "$SERVICE_ACCOUNT_KEY" https://asia.gcr.io
    # Push the image
    - docker push ${IMAGE_TAG}:${IMAGE_VER}
  only:
    refs:
      - develop
    changes:
      - Dockerfile
      - bin/*
      - conf/*
      - requirements.txt
  • ジョブ定義②
    ここではテスト/本番環境に対する2つのCronJobリソースへのデプロイを行います。
    • 本番環境へはwhen: manualを定義して手動でデプロイするようにしています。
deploy_model_builder_dev:
  extends: .setting_dev
  image: 'claranet/gcloud-kubectl-docker:1.2.2'
  stage: deploy
  tags:
    - gke
  script:
    # Authenticate with GKE
    - echo "$SERVICE_ACCOUNT_KEY" > key.json
    - *auth_gke
    # set up CronJob
    - cat kubernetes/XXXX-job.yaml | envsubst | kubectl apply -f -
  only:
    refs:
      - develop
    changes:
      - .gitlab-ci.yml
      - kubernetes/XXXX-job.yaml

deploy_bulk_judgement_dev:
  extends: .setting_dev
  image: 'claranet/gcloud-kubectl-docker:1.2.2'
  stage: deploy
  tags:
    - gke
  script:
    - echo "$SERVICE_ACCOUNT_KEY" > key.json
    - *auth_gke
    # set up CronJob
    - cat kubernetes/XXXX-job.yaml | envsubst | kubectl apply -f -
  only:
    refs:
      - develop
    changes:
      - .gitlab-ci.yml
      - kubernetes/XXXX-job.yaml

deploy_model_builder_prod:
  extends: .setting_prod
  image: 'claranet/gcloud-kubectl-docker:1.2.2'
  stage: deploy
  tags:
    - gke
  script:
    # Authenticate with GKE
    - echo "$SERVICE_ACCOUNT_KEY_PROD" > key.json
    - *auth_gke
    # set up CronJob
    - cat kubernetes/XXXX-job.yaml | envsubst | kubectl apply -f -
  only:
    - master
  when: manual

deploy_bulk_judgement_prod:
  extends: .setting_prod
  image: 'claranet/gcloud-kubectl-docker:1.2.2'
  stage: deploy
  tags:
    - gke
  script:
    # Authenticate with GKE
    - echo "$SERVICE_ACCOUNT_KEY_PROD" > key.json
    - *auth_gke
    # set up CronJob
    - cat kubernetes/XXXX-job.yaml | envsubst | kubectl apply -f -
  only:
    - master
  when: manual

ロールバック

せっかくなのでロールバックの手順についても記載しておきます。 Gitlabの左メニューから運用 -> 環境とたどっていくと、下記のような画面が表示されます。

上記で記載した.gitlab-ci.ymlにenvironmentという定義があると思いますが、 そこで定義した文字列が環境のところに表示されているのがわかると思います。 たとえばここでproductionをクリックします。 すると下記の画面に遷移します。

この一覧はproductionに対するコミットの一覧です。 たとえば直前のコミットにロールバックしたい場合は赤枠のところをクリックすればOKです。 もちろん、この画面から任意のコミットを選択して再デプロイすることも可能です。

environmentの定義があることで環境毎のコミット履歴が一覧で見れるかつ、 そこからのデプロイ、ロールバックも簡単に行えます。 複数環境がある場合にはぜひ設定することをおすすめします。

まとめ

  • 今回、Gitlab CIを初めて利用してみましたが、特に癖もなく使いやすかったです。
  • Auto DevOpsや、先日発表されたGitLab Serverlessなど、まだまだアツい機能があるので、時間を見つけて検証してみようと思います。

  1. 当初はGitlabのAuto Devopsの利用を考えていましたが、検討時点では利用しているGitlabのバージョンが古かったため、ひとまずGitlab CIを利用することにしました。 

日報をword cloudで可視化して2018年を振り返る

この記事は Enigmo Advent Calendar 2018 の19日目の記事です。

はじめに

ネタ何にしようかなぁと思って、

  • カジュアルな感じでかつ単発で終わるようなもの
  • 検索、自然言語処理関連で何か
  • 年末的な何か

ということを踏まえて、 Qiitaチームに日々挙げている自分の作業日報を可視化して2018年の振り返りをしてみることにしました。

私がエニグモに入社したのが今年の2月なので、正確には1年ではないですが、 細かいことは気にせずいきたいと思います。

作業フロー

作業フローとしては、

  • Qiita API v2を利用して自分の作業日報の本文と作成日時を取得
  • 取得した本文を形態素解析器にかけてキーワードのみを抽出
  • 抽出したキーワードをword cloudを利用して可視化
  • 可視化したものを見て思い出に浸る

という感じで進めようと思います。

Qiita APIで日報の本文を取得

  • アクセストークンの発行 Qiitaチームからデータを取得するためアクセストークンを発行します
  • アクセストークンの利用
    • 取得したアクセストークンはAuthorizationリクエストヘッダに指定して利用します。
    • 今回はpythonで実装しましたが、curlの場合は下記のような感じで取得できます。
$ curl https://qiita.com/api/v2/authenticated_user/items \
-H "Authorization: Bearer [アクセストークン]"
  • 下記のメソッドを定義して日報データを取得します
    • APIを利用して20件ずつデータを取得
    • 取得したデータはDataframeに突っ込み、不要なデータは取り除く
    • 取得した作成日時をDatetimeIndexに設定して、月単位でデータが扱えるようにしておく
def get_report():
    BASE_URL = 'https://XXXX.qiita.com/api/v2/items?'
    PER_PAGE = 20
    # tagで絞り込み
    QUERY = 'tag%3A%E6%97%A5%E5%A0%B1%2F%E4%BC%8A%E8%97%A4%E6%98%8E%E5%A4%A7'
    curr_page = 1
    with open('qiita_access_token.txt') as f:
        qiita_access_token = f.read().strip()
    header = {'Authorization': 'Bearer {}'.format(qiita_access_token)}
    
    
    # レスポンスヘッダを見てページング処理
    df_list = []
    total_cnt = 0
    while(True):
        target_url = '{0}page={1}&per_page={2}&query={3}'.format(BASE_URL, curr_page, PER_PAGE, QUERY)
        req = urllib.request.Request(target_url, headers=header)
        res = urllib.request.urlopen(req)
        if curr_page == 1:
            # ヘッダから全投稿数を取得
            total_cnt = int(res.info()['Total-Count'])
            # 最終ページの計算
            if total_cnt % PER_PAGE == 0:
                last_page = total_cnt // PER_PAGE
            else:
                last_page =  total_cnt // PER_PAGE + 1
        res_body = res.read()
        res_dict = json.loads(res_body.decode('utf-8'))
        
        # dataframeに変換
        df_list.append(pd.io.json.json_normalize(res_dict))
    
        # 最終ページチェック
        if curr_page == last_page:
            break
        curr_page+=1
        
    # df_listを結合
    report_df = pd.concat(df_list, ignore_index=True)
    
    # 必要なデータのみ抽出
    report_df = report_df[['body', 'created_at']]
    
    # created_atの文字列をdate型に変換
    report_df['created_at'] = pd.to_datetime(report_df['created_at'])
    
    # DatetimeIndexに変換
    report_df.set_index('created_at', inplace=True)
    
    return report_df

形態素解析器でtokenize&前処理

  • janome(辞書はneologd)を利用し、前処理として下記を実施します。
    • unicode正規化
    • 改行コード削除
    • 大文字→小文字に統一
    • 名詞のみ抽出(数字は対象外)
def analyze():
    
    # charfilter
    ## unicode正規化
    ## 改行コード削除
    charfilters = [UnicodeNormalizeCharFilter(),
                           RegexReplaceCharFilter(u'\r\n', u'')]

    # tokenizer
    tokenizer = Tokenizer(mmap=True)

    # tokenfilter 
    ## 大文字→小文字
    ## 名詞のみ抽出(数は除去)
    tokenfilters = [POSKeepFilter('名詞'), POSStopFilter(['名詞,数']), LowerCaseFilter()]
    # analyzer
    analyzer = Analyzer(char_filters=charfilters, tokenizer=tokenizer, token_filters=tokenfilters)
    
    return analyzer


def get_word_list(report_list, analyzer):
    word_list = []
    for report in report_list:
        word_list.append(" ".join( analyzed_line.base_form for analyzed_line in analyzer.analyze(report)))
    return word_list

可視化処理

word cloudを利用して可視化するためのメソッドを定義します

def draw_wc(vecs_dic, fig_title):
    font_path = '/System/Library/Fonts/ヒラギノ角ゴシック W3.ttc'
    wordcloud = WordCloud(background_color='white',
                          font_path = font_path,
                          min_font_size=15,
                          max_font_size=200,
                          width=1000,
                          height=1000)
                          
    wordcloud.generate_from_frequencies(vecs_dic)
    plt.figure(figsize=[20,20])
    plt.imshow(wordcloud,interpolation='bilinear')
    plt.axis("off")
    plt.title(fig_title,fontsize=25)
    plt.show()

指定した月の日報を可視化する

では実際にやっていきます。 まず、私が入社した月の日報を見てみましょう。

  • 1年間分の日報データを取得
df_reports = get_report()
  • 可視化したい月を指定
target_month = '2018-02'
my_analyzer = analyze()
words = get_word_list(df_reports[target_month].body.tolist(), my_analyzer)
  • TF-IDFで各単語を重み付け
vectorizer = TfidfVectorizer(use_idf=True, token_pattern=u'(?u)\\b\\w+\\b')
vecs = vectorizer.fit_transform(words)
words_vectornumber = {}
for k,v in sorted(vectorizer.vocabulary_.items(), key=lambda x:x[1]):
    words_vectornumber[v] = k
vecs_array = vecs.toarray()
all_reports = []
vecs_dic = {}

for vec in vecs_array:
    words_report = []
    vector_report = []
    for i in vec.nonzero()[0]:
        vecs_dic[words_vectornumber[i]] = vec[i]
  • 可視化
draw_wc(vecs_dic, target_month)

「入社」というキーワードでフレッシュ感がほのかに漂っています。 その他でみると「性能」、「分析」、「検証」、「測定」とかが大きくでてますね。 そういえば、この月は検索システムの性能改善をメインでやっていた気がします。

続いて6月の日報を見てみます。

「韓国」と大きく出てますが、これな何でしょうかね。全く覚えがありません(笑) 「chef」、「レシピ」、「zero」とかはこのあたりでchef zeroで何かの検証をしていたんでしょう。きっと。。 「パーソナライズド」、「fasttext」があるので、この時期あたりからパーソナライズサーチの検証をしてたんだと思います。

最後に11月

他人の日報なんて誰も興味ないと思うので、これで最後にします。 せっかくなのでクリスマスっぽくしてみました。

先月の内容なので、特に振り返ることもありませんが、 Gitlab CI周りの調査/検証やAPIの開発をメインで実施していた時期でした。

おわりに

今回初めてword cloudを使ってみましたが、 視覚的にデータを見るのって新たな気付きがあったりして楽しいですね。 来年はフォントとか画像をもうちょっとオサレな感じにして日報Tシャツ作ってヘビロテしようと思います。

Sassで見出しデザインの@mixinを作ろう!

こんにちは、@pompom0c0 です。 この記事は Enigmo Advent Calendar 2018 の18日目の記事です。 17日目の記事は @natten の はじめてのキーボード設計におけるアンチパターン でした。

今日はBUYMA内で使用している見出しデザインついて紹介して行こうと思います。

前置き

今回この記事を書こうと思った経緯

  • Sassの@mixinを実践でどんな風に使っているか知ってほしい!
  • 使う見出しデザインがパターン化されているので、デフォルトであると便利だと思ったから

想定読者

  • LPの実装をするデザイナーの方
  • @mixinは自分から書いたことがない人。

今回扱わないこと

  • FLOCSSの設計について
  • @extend@contentでの書き方紹介

mixinのおさらい

  • 別で定義したスタイルを@includeでなんども呼び出すことができます。
  • 引数を指定して、関数的な使い方もできます。
@mixin hoge($color: #fff, $size: 1rem) {
  color: $color;
  font-size: $size;
}
.ttl__1 {
  @include hoge(#000, 2rem);
}
.ttl__2 {
  // 引数は初期値のを使用
  @include hoge();
}
.ttl__1 {
  color: #000;
  font-size: 2rem;
}
.ttl__2 {
  color: #fff;
  font-size: 1rem;
}

引数を使えば初期値で出力するだけでなく、「今回はもう少し違った雰囲気で見せたいな...」という際には値を変えれば デフォルトでCSSを設定していても、出力結果を冗長させることなく記述することができます。

あとはよくある見出しデザインをSassでまとめて記述しておきます。

よく使う見出しデザインSass

縦線

@mixin line--ver($color: #000, $size: 30px) {
  &::after {
    content: '';
    width: 1px;
    margin-top: 1em;
    margin-bottom: .5em;
    display: inline-block;
    // 線の色と長さを引数にする
    height: $size;
    border-right: solid 1px $color;
  }
}

横線

@mixin line--under($color: #000, $size: 1px) {
  position: relative;
  &::before {
    content: '';
    position: absolute;
    left: 50%;
    bottom: -15px;
    display: inline-block;
    width: 60px;
    -moz-transform: translateX(-50%);
    -webkit-transform: translateX(-50%);
    -ms-transform: translateX(-50%);
    transform: translateX(-50%);
    // 線の色と長さを引数にする
    height: $size;
    background-color: $color;
  }
}

文字横に線

@mixin line--side($color: #000, $size: 1px) {
  border-left: solid $size;
  border-right: solid $size;
  width: 45%;
  margin: 0 auto 1em;
  padding-bottom: 0;
  border-color: $color;
}

文字下に蛍光ペンでマーキング

@mixin line--maker($color: #ffc63b, $size: .2rem) {
  background: linear-gradient(transparent 60%, $color 60%);
  padding: 0 $size;
}

コンテンツの幅の分だけ色がついてしまうので、マーキングしたい箇所にのみspanタグでclass指定してください。

<div class="framework__ttl">
  <h2><span class="ttl">タイトル</span></h2>
</div>

鍵カッコ

@mixin mark--quo($color: #000, $size: 1px) {
  position: relative;
  padding: 1rem;
  &amp;::before, &amp;::after {
      content:'';
      width: 20px;
      height: 30px;
      position: absolute;
      display: inline-block;
  }
  &amp;::before {
      border-left: solid $size $color;
      border-top: solid $size $color;
      top: 0;
      left: 0;
  }
  &amp;::after {
      border-right: solid $size $color;
      border-bottom: solid $size $color;
      bottom: 0;
      right: 0;
  }
}

まとめ

久しぶりに使う見出しデザインを実装する直前になって 「あ、これどうやって書くんだっけ・・・」とググるのは生産的じゃないな〜と思ってまとめてみました。 @mixin使ってるけど引数までは使ってない!使いこなしてみたい!って方に参考になればと思います。

参考サイト

Sass(SCSS)のmixin, extendなどまとめ Sassで@mixinを作る時に知っておきたい基礎知識 より素早くCSSコーディングするための、Sass(SCSS)のmixinスニペット集

はじめてのキーボード設計におけるアンチパターン

こんにちは。Enigmo インフラグループの @natten です。 この記事は Enigmo Advent Calendar 2018 の17日目の記事です。 16日目の記事は @enigmo7 の デザインパターンとリファクタリング でした。

本日の記事は技術寄りの話ではなく趣味の世界、自作キーボードのお話です。

キーボードを設計するモチベーション

2015年登場のErgoDoxに端を発する(諸説あります)メカニカルキーボード自作のムーブメントは国内でも盛んになる一方で、今年はHelixやErog42, Mint60にCrkbdなどなどエポックメイキングなプロジェクトが多く登場しました。 自分もご多分に漏れずこの潮流に飲み込まれてしまい、キーボードを購入するために技術書典に足を運んだり、キーキャップ一式を購入してみては「こんなプラスチックの塊が2万円?!」と愕然とする日々です。

さて、しばらくキーボードを買ったり作ったり積んだりしていると、不思議なもので「自分でもキーボードを設計してみようかな?」という気持ちが芽生えてきます。 様々なキーボードのキー配置に触れているうちに好みのキー配列や指の可動域、多用するキーのポジションがわかってきて自分に最もフィットしたキーボードは自分にしか作れない、という結論になりました。

とにかくキーボードを設計する

じゃあ実際にキーボードを設計してみましょう、ということで教則本を買います。

Crkbdを設計した @foostan さんによる、自作キーボード設計ノウハウ集です。 KiCADの使い方からケースの設計、PCBの発注と自作キーボード作成に必要な作業の一通りがわかりやすく記述された良書です。

こちらを斜め読みして、とにかく勢いとお試し感覚で設計してできあがった基板の実物がこちらになります。

https://pskbd.booth.pm/items/1044084

自作キーボードの世界に明るくない方はなんだこれ、となるでしょう。 じゃあ自作キーボード界の人ならどうかというと、同じくなんだこれ、という感想になります。 パーツを仮置きしてみると不思議がさらに加速していきます。

はみ出るように不自然に配置されたProMicroスペース、分離型でもないのにリバーシブル基板、キーと平行するように伸びるUSBケーブル。 なんでこんなことになってしまったのか、と冷静に振り返ってみると思い当たる節が次から次へと。

といったわけで、今回は

こんなキーボードができたよ!すごいだろ!

ではなく、

こんなキーボードができてしまった!こんなことしなければよかった!

という点をご紹介します。

キーボード設計アンチパターン

コンセプトを山盛りにする

  • 3列キーボードとか4列キーボードに追加できる、数字キー/ファンクションキー代わりの1列キーボードが欲しいな。数字キーだから0〜9で10キーかな
  • キースイッチテスターってただのアクリルベース土台タイプが多いけど、打鍵感だけじゃなくて接点の違いとかも確認しようと思うと実際のキー入力が必要になるよね。ホットスワップタイプにすれば実用的なテスターとしても使えるんじゃない?
  • 普段は使わないけど気まぐれに光らせたいときがあるなー。表面実装のLEDはハードルが高いから、とりあえずUnderGlowのLEDテープが取り付けられるようにパッドだけ用意しとこ
  • モゲMicro怖いからProMicroソケット化したいし、もしものときに取り外せるようにトッププレートで覆わないようにしておいたほうがいいかな

ダメなところ

  • そんなにいっぱい並立できません
  • 机上の空論なので、実際に基板を作るとコンセプト間で対立・矛盾が発生する

改善方法

  • 各コンセプトの要素(実用,趣味,利便,実験,コストなど)のバランスを見直す。特に実験方向にステータスを振れば振るほど破綻する可能性が高くなる
  • コンセプトはなるべく絞ると設計しやすい。反面、凡庸なものになってモチベーションが下がることもあるので、少しだけ背伸びするのも良い

何も考えずにリバーシブル基板にする

今後左右分離型のキーボードを作ることになるだろうし、多くの分離型キーボードと同じようにリバーシブル基板にしよう! 気分や環境によって選べるのは利点だよね?

ダメなところ

  • MCU周辺が無秩序なビアだらけになって汚い
  • リバーシブルにしたばかりに配線がめちゃくちゃ面倒になる
  • そもそも意味がない

改善方法

  • 作ろうとしているキーボードの用途や量産体制、目的がリバーシブル基板に見合っているかよく考える
  • 実例が多いからという雑な理由でリバーシブルにするより、片面できれいな配線方法を模索するのも一つの手です

基板以外の構成パーツについて深く考えない

いろんなキーボードのソースデータからフットプリントのパーツも流用して基板の設計ができたぞ! 実際にフットプリントを置いて配線してみると、どうにもならない箇所があったから多少妥協して当初のイメージとは違う形になったけど、実物が見たいからとりあえず発注しよう。リセットスイッチとかスペーサーはあとで検討して調達すればいいや

ダメなところ

  • なんとなく流用したパーツの入手性やコスト、サイズ感を意識していない
  • 平面上の寸法しか見ていないと、サンドイッチタイプのケースで物理的な干渉が発生することが多い

改善方法

  • 実際にキーボードとして組み立てる場合のパーツ構成と具体的な寸法を書き出してみる
  • 参考にしたキーボードが手元にあったら採寸して、パーツ配置の意図を読み取る
  • 使用する可能性のあるパーツは検証用に一通り手元に揃えておく

二言目には未完成とか言い出す

最低限は形になったけど、ダメな点もいっぱいあるしGithubで公開するのはちょっと気が引けるな…マサカリ飛んできたら心が折れちゃうかもしれないし、しばらく手元で温めようかな

ダメな点

そういうこと言ってるやつは永久に完成させられない

改善方法

とにかくプロジェクト名を決めてGithubに空リポジトリを作れ、Twitterとか自作キーボードDiscordでガンガン進捗を上げてバンバン叩かれよう

アンチパターンを振り返って

よくよく振り返ってみると、どれもキーボード設計に限ったことではありませんね。 プロダクト開発やシステム設計でもありがちな話ばかりです。

  • コンセプトを山盛りにする
    • →理想を高く掲げすぎず、実装する要素を精査する
  • 何も考えずにリバーシブル基板にする
    • →一般事例に惑わされず、自分の目的を見直す
  • 基板以外の構成パーツについて深く考えない
    • →細部にとらわれず、最終的なビジョンと全体像を考えて念入りに準備をする
  • 二言目には未完成とか言い出す
    • →初手から完璧を求めず、スモールスタートでもいいから人の目に晒して育てる

何かを利用・享受する側だけではなく、作り手の側に回り実際に手を動かしてみると、趣味の世界からでも得るものがあるという学びでした。 自分と同じようにはじめてキーボードを設計してみようかな、と考えている方の参考になれば幸いです。


Enigmoでは何かを作り出すことに熱中できる仲間を募集しています。 Adevent Calendarの記事ラインナップからもわかるように、バラエティに富んだメンバーがお待ちしていますので、自作キーボードを一緒にワイワイやってくれる方の ご応募お待ちしております。

明日の記事の担当は@pompom0c0です。お楽しみに。

デザインパターンとリファクタリング

この記事はEnigmo Advent Calendar 2018の16日目です

デザインパターンリファクタリング

こんにちは。iOSチームでエンジニアをやっています

今チームでは、プロジェクトの進行と並行してリファクタリングを行なっています

対象プロジェクトは、MVVMのデザインパターンを多用しています

そのプロジェクトをどうリファクタリングしていったかをツラツラと書いていこうかと思います

TL;DR

  • デザインパターンって、設計パターン。うまく対処するためにどう設計していけばいいかをまとめたものです
  • デザインパターンを各画面で分けよう
  • 簡単な実装なのに、MVVMを利用したら複雑になってしまった、であれば、設計を間違えている

リファクタリング

リファクタリングについては、短期間で見ると、ビジネスとしてぶっちゃけ一円にもなりません。 しかし、リファクタリングをする、しないでは、未来のプロジェクトの進行速度に影響していきます。

なぜリファクタリングをしたのか

  • メンテ、新規開発がしづらい
  • 数行いじると全然違う画面でエラーがでてしまう
  • どの画面がどのViewControllerなのかわからない
  • Swiftらしいコードに
  • MVVMなのに、UIViewControllerがふとっている

着手前

[アプリ設計]

  • RxSwift / RxSwift Community の様々なライブラリを使って MVVM を 試してみた実装
  • iOS5の時代のライブラリをそのまま使い続け、iOSの新しい機能が実装されていない

[構成]

  • Managers
    • サーバAPIへアクセスする
    • レスポンスをModelへパースする
    • パースしたModelの一部を、インスタンス変数で管理
    • シングルトン
  • ViewController
    • 別ViewControllerに遷移
    • Userのアクションへのリアクション
  • View
    • SnapKitによるレイアウト実装
  • ViewModel
    • Managerにデータを要求、受け取ったデータを管理
    • UIViewControllerにアクセスしてUIの更新等を行う
  • Model
    • データ

[アプリ設計]

[構成]

  • Components
    • Model
    • データ
    • ViewController
    • 別ViewControllerに遷移
    • Userのアクションへのリアクション
    • Delegate / DataSourceの実装
    • Storyboard / Xib
    • レイアウト実装
    • View
    • iPhoneのサイズによるFontサイズの調整など
  • Network
    • APIClient
    • サーバに要求する
    • Responseを生成する
    • Response
    • Codableによって APIをモデル化

比較

Before

After

やったこと

  1. Componentsというディレクトリを作ってその中に各画面毎のfileをいれるようにしました
  • その画面に関係するfileが明確になり、関係ないものは使わないようにチームで心がけるようになりました
  • 各Componentでデザインパターンを変更できるようにしたので、あった設計をできるようにしました
  • 簡単な画面については、コード量が少ない設計に変更
  1. APIClient / JSONデコーダー ともに複数あったので、新しいAPIClientを作成し、Codableを使うように変更 -> 古いものはまとめて削除。Objective-Cの時のような、json["key"] as? Intのような実装をなくしました

  2. ソースコードに対するコメント / BTS なぜその実装になったのか等記載がない -> 現状を知り着手しやすいように、複雑な処理になる部分は、シーケンス図 / コメントで動作を記載

  3. 一行直すだけで、関係ないと思ったところでエラーが起こる -> Component間で扱うデータを減らし、非結合にしてComponent間の影響を減らしました

  4. Manager / Utility クラスは、シングルトンで実装 -> シングルトン実装のクラスを極力減らす。シングルトンにするとpropertyをつけたくなる人がいるので避けます

  5. UIはSnapKitのみで実装 -> Storyboard / Xib で実装、IBInspectable, IBDesignableを使い、GUIで状況を把握しつつ実装  デザイナーが作ってくれているレイアウトに沿った物を作れる ( SnapKitだけの時、cornerRadiusなど漏れが発生していた )

まとめ

よかったこと

  • コードの削除をかなりできた 着手前と今の差分:

    2467 files changed, 142326 insertions(+), 271177 deletions(-)
  • すべての画面に、MVVMをあてようとして無理している部分が多々あり結果、読みにくいコードになっていたのを直せた

  • レビューする際に、コードよりも、Storyboard / Xib で見た方がわかりやすかった Before:

    override func viewDidLoad() {
    super.viewDidLoad()
    .... 40行ほど
    }
    
    After:
    override func viewDidLoad() {
    super.viewDidLoad()
    imageView.image = UIImage(named: ... ) // 1行のみ
    }
    

  • ドキュメント作成した事で効率改善が行えた

    • デザイナーとの画像受け渡しは、Xcodeから直接やってもらえることになった
    • ドキュメント自体もレビューされるので、その際に共有できた
    • レビューする側も、どういう事をしたいロジックなのか理解できた

リファクタリングを行う時、考慮すること

  • チーム全員なので、非エンジニアにも理解してもらわないといけない
  • リファクタしたいところを共有しておく
  • 企画・ディレクター案件がある際、その画面対応の際にまとめて行う
  • ディレクターには、案件を画面毎にまとめるように整理してもらう

e.g. - ホームが重いので改善する作業中に、haptic feedbackの導入や、お気に入りのハートを他の画面と同期するように仕組みを入れました - 検索画面を改修する際に、保存した検索条件に起因する部分もまとめて、書き直し整理を行ったりしました

Chainerでまるばつゲームを学習させてみた

はじめに

エニグモ サーバーサイドエンジニアの @gugu です。 この記事はEnigmo Advent Calendar 2018の15日目です。

日頃はBUYMAの機能改修を行っていますが、弊社では月末のプレミアムフライデーは業務と関係ない開発を行って良い日となっています。 そこで、前から興味のあった機械学習で何か作ってみようと思いました。

Chainerを使って「まるばつゲーム」を学習させてみたので、簡単にやったことを書こうと思います。

github.com

※ちょくちょくリファクタするかもです。

本題に入る前に

私のスペック

  • 日頃はRailsでサーバーサイドの開発
  • pythonはラズパイでLチカをしたぐらい
  • はじめての機械学習

仕様など

  • みんな知っているまるばつゲームです。
  • 学習させるコンピュータは常に先手
  • 0〜8の計9マス。どこに打つかを学習させます。(もう既に打たれたマスにも打つことが可能。当然ルール違反なので負け)
     |     |
  0  |  1  |  2
     |     |
-----+-----+-----
     |     |
  3  |  4  |  5
     |     |
-----+-----+-----
     |     |
  6  |  7  |  8
     |     |
  • 盤面のデータは誰も打ってないマスは0、○は1、☓は2の値が入ります。
     |     |
     |     |  ○
     |     |
-----+-----+-----
     |     |
  ☓  |  ○  |  ○
     |     |
-----+-----+-----
     |     |
     |  ☓  |  ☓
     |     |

      ↓ ↓ ↓

     |     |
  0  |  0  |  1
     |     |
-----+-----+-----
     |     |
  2  |  1  |  1
     |     |
-----+-----+-----
     |     |
  0  |  2  |  2
     |     |

配列だと[0, 0, 1, 2, 1, 1, 0, 2, 2]

  • コンピュータと戦わせて学習させます。(完全ランダムに打つ。打たれているマスには打たない。不毛な戦いになるので。)
  • 盤上の状態を入力して、出力結果のうちの最大値のマスに打ちます
     |     |
     |     |  ○
     |     |
-----+-----+-----
     |     |
  ☓  |  ○  |  ○
     |     |
-----+-----+-----
     |     |
     |  ☓  |  ☓
     |     |

のとき入力値は[0, 0, 1, 2, 1, 1, 0, 2, 2]
出力結果がもし[0.1, 0.2, 0.4, 0.2, 0.1, 0.3, 0.9, 0.1, 0.5]だったら、数値が最大のマス6に打ちます。

  • 勝敗結果から打ったマスが好手だったのか悪手だったのかを教えます

Chainerの基礎知識

ニューラルネットを定義

from chainer import Link, Chain, ChainList
import chainer.functions as F
import chainer.links as L

class MyChain(Chain):
    def __init__(self):
        super(MyChain, self).__init__(
                # 9-20-9の3層(隠れ層の20はなんとなく。)
                l1 = L.Linear(9, 20),
                l2 = L.Linear(20, 9)
        )

    def __call__(self, x):
        # 伝播(sigmoidでも良いがleaky_reluの方が結果が良いような気がする。)
        h = F.leaky_relu(self.l1(x))
        o = self.l2(h)
        return o

初期設定

# ニューラルネット
model = MyChain()

# 確率的勾配降下法(Stocastic Gradient Descent)を使用
opt = optimizers.SGD()
opt.setup(model)

打つ場所を決める

# 盤上の状態データをfloat32に形成する(chainerがfloat64には対応していないため)
x = Variable(np.array([input_data], dtype=np.float32))

# 勾配を0に初期化(chainerのお決まりごと)
model.zerograds()

# 入力xを変換し出力yへ
y = model(x)

# 出力の最大値を打つ
y.data.argmax()

学習させる

# 教えるデータをfloat32に形成する
t = Variable(np.array([teacher_data], dtype=np.float32))

# 出力yと教えるデータtとの差分を算出(平均二乗誤差)
loss = F.mean_squared_error(y, t)

# 逆伝播
loss.backward()

# 最適化
opt.update()

上記の基礎知識を使ってまるばつゲームを学習させていきます。

学習方法

強化学習はよく理解していないので、基礎知識のみで自己流に学習させます。

# result: 勝敗結果
#   ドロー:0
#   勝ち:1
#   負け:2
#   既に打たれたマスに打った:3
def learn(self, result):
    for i, y in enumerate(models):
        # marks[i]: 打ったマス番号
        # y.data[0]: 出力値
        teacher = self.teacher(result, marks[i], y.data[0])
        t = Variable(np.array([teacher], dtype=np.float32))

        # 出力yと正解tとの差分を算出(平均二乗誤差)
        loss = F.mean_squared_error(y, t)

        # 逆伝播
        loss.backward()

        # 最適化
        opt.update()

    # 学んだらリセット
    del models[:]
    del marks[:]
def teacher(self, result, mark, model):
    data = []
    # draw, win
    if result == Settings.WIN:
        for i in range(9):
            if i == mark:
                # 打ったマスに値を与える
                data.append(1)
            else:
                # 打ってない箇所は現状維持
                data.append(model[i])
    # lose or same place
    elif result == Settings.LOSE or result == Settings.SAME_PLACE:
        for i in range(9):
            if i == mark:
                data.append(-1)
            else:
                data.append(model[i])
    # draw
    else:
        for i in range(9):
            if i == mark:
                data.append(0)
            else:
                data.append(model[i])
    return data

学習方法説明

勝った場合

     |     |
     |     |  ○
     |     |
-----+-----+-----
     |     |
  ☓  |  ○  |  ○
     |     |
-----+-----+-----
     |     |
  ○  |  ☓  |  ☓
     |     |

最後の一手の出力結果が[0.1, 0.2, 0.4, 0.2, 0.1, 0.3, 0.9, 0.1, 0.5]で、マス6に打った場合、

  • 出力結果: [0.1, 0.2, 0.4, 0.2, 0.1, 0.3, 0.9, 0.1, 0.5]
  • 教えるデータ: [0.1, 0.2, 0.4, 0.2, 0.1, 0.3, 1, 0.1, 0.5]

とマス6の値を1にします。

同じように3手目が[0.3, 0.1, 0.9, 0.7, 0.5, 0.3, -0.3, -0.8, 0.5]で、マス2に打っていた場合

  • 出力結果: [0.3, 0.1, 0.9, 0.7, 0.5, 0.3, -0.3, -0.8, 0.5]
  • 教えるデータ: [0.3, 0.1, 1, 0.7, 0.5, 0.3, -0.3, -0.8, 0.5]

とマス2の値を1にします。

※これを1手目まで繰り返します。 ※上記は勝った例なので1にデータを変換しましたが、負けた場合は-1に、ドローは0にデータになります。

学習させてみた

※データは最後から100戦の勝率

100戦

win: 0.04
lose: 0.04
draw: 0.0
same_place: 0.92

1,000戦

win: 0.22
lose: 0.01
draw: 0.0
same_place: 0.77

10,000戦

win: 0.61
lose: 0.06
draw: 0.0
same_place: 0.33

100,000戦

win: 0.81
lose: 0.08
draw: 0.02
same_place: 0.09

強くなってる!

VS 人間

実際に100,000戦したコンピュータと戦ってみました。 ※コンピュータ:○、 人間:☓

     |     |
  0  |  1  |  2
     |     |
-----+-----+-----
     |     |
  3  |  ○  |  5
     |     |
-----+-----+-----
     |     |
  6  |  7  |  8
     |     |

      ↓ ↓ ↓

     |     |
  0  |  1  |  x
     |     |
-----+-----+-----
     |     |
  ○  |  ○  |  5
     |     |
-----+-----+-----
     |     |
  6  |  7  |  8
     |     |

      ↓ ↓ ↓

     |     |
  ○  |  1  |  x
     |     |
-----+-----+-----
     |     |
  ○  |  ○  |  x
     |     |
-----+-----+-----
     |     |
  6  |  7  |  8
     |     |

      ↓ ↓ ↓

     |     |
  ○  |  1  |  x
     |     |
-----+-----+-----
     |     |
  ○  |  ○  |  x
     |     |
-----+-----+-----
     |     |
  6  |  7  |  x
     |     |

まともに戦えるが、弱い。。

学習方法を変えてみた

どう変えたか?

「既に打たれたマスに打った」で負けた場合は、最後の一手のデータを調整するように変更します。

ソース

# result: 勝敗結果
#   ドロー:0
#   勝ち:1
#   負け:2
#   既に打たれたマスに打った:3
def learn(self, result):
    for i, y in enumerate(models):
        # 既に打たれたマスに打った and 最後の一手
        if result == 3 and i == len(models) - 1:
            # marks[i]: 打ったマス番号
            # y.data[0]: 出力値
            teacher = self.teacher(result, marks[i], y.data[0], True)
        else:
            teacher = self.teacher(result, marks[i], y.data[0], False)

        t = Variable(np.array([teacher], dtype=np.float32))

        # 出力yと正解tとの差分を算出(平均二乗誤差)
        loss = F.mean_squared_error(y, t)

        # 逆伝播
        loss.backward()

        # 最適化
        opt.update()

    # 学んだらリセット
    del models[:]
    del marks[:]
def teacher(self, result, mark, model, last_flg):
    data = []
    # draw, win
    if result == Settings.WIN:
        for i in range(9):
            if i == mark:
                # 打ったマスに値を与える
                data.append(1)
            else:
                # 打ってない箇所は現状維持
                data.append(model[i])
    # lose
    elif result == Settings.LOSE:
        for i in range(9):
            if i == mark:
                data.append(-1)
            else:
                data.append(model[i])
    # same plase
    elif result == Settings.SAME_PLACE:
        if last_flg == True:
            for i in range(0, 9):
                if i == mark:
                # 最後に打ったマスだけを`-2`に調整する。
                    data.append(-2)
                else:
                    data.append(model[i])
        else:
            for i in range(0, 9):
                data.append(model[i])

    # draw
    else:
        for i in range(9):
            if i == mark:
                data.append(0)
            else:
                data.append(model[i])
    return data

学習させてみた

10,000戦

win: 0.96
lose: 0.04
draw: 0.0
same_place: 0.0

コンピュータ相手だと9割以上勝てるようになって、打たれているマスには打たなくなりました。

VS 人間 (2)

100,000戦したコンピュータともう一度戦ってみました。 ※コンピュータ:○、 人間:☓

     |     |
  0  |  1  |  2
     |     |
-----+-----+-----
     |     |
  3  |  ○  |  5
     |     |
-----+-----+-----
     |     |
  6  |  7  |  8
     |     |


     |     |
  0  |  ○  |  2
     |     |
-----+-----+-----
     |     |
  3  |  ○  |  x
     |     |
-----+-----+-----
     |     |
  6  |  7  |  8
     |     |


     |     |
  0  |  ○  |  ○
     |     |
-----+-----+-----
     |     |
  3  |  ○  |  x
     |     |
-----+-----+-----
     |     |
  6  |  x  |  8
     |     |


     |     |
  x  |  ○  |  ○
     |     |
-----+-----+-----
     |     |
  3  |  ○  |  x
     |     |
-----+-----+-----
     |     |
  ○  |  x  |  8
     |     |

強い!まだ最強とまでは言えませんが、2つリーチを作れることを覚えたようです。

まとめ

機械学習はハードルがものすごく高いイメージでしたが、Chainerの基礎的な関数を駆使すれば初心者の私でも簡単な機械学習を作成できることがわかりました。 今後は強化学習DQNなど)をちゃんと勉強して、負けないレベルに修正できたらと思います。