第3回定期勉強会「PM先生 俺みたいになるな!!」

エキサイトの おおしげ です。

今月も定期勉強会が開催されたのでそれについて共有しようと思います。

過去開催分についてはこちらからどうぞ
tech.excite.co.jp
tech.excite.co.jp

「PM先生 俺みたいになるな!!」

f:id:excite-ohshige:20210823184858p:plain:w500

今月は「PM先生 俺みたいになるな!!」というタイトルで、普段からプロジェクトマネジメントを行っているiXITメンバーによるプロジェクトマネジメント全般に関するお話でした。

主な内容として、

  • プロジェクトマネジメントを初めて経験する場合のあるある
  • それを防ぐためのPMBOKの紹介
  • 過去に経験したプロジェクトマネジメントに関する大きなしくじり
  • 実際にプロジェクトマネジメントを行っているメンバーからのお悩み相談

などがありました。

エキサイトとiXITでは自社開発もあれば受託開発もあって、大なり小なりプロジェクトマネジメントに関わっているメンバーは多数いるので、皆とても勉強になったようです。
やはり内容として堅苦しいものになってしまうかとも思いましたが、発表していただいたメンバーがわかりやすく楽しい発表にしてくれたのでとても盛り上がりました。
特に、あるあるの話のときは身にしみたり、しくじりの話のときはそれを反面教師にしたりと、コメント欄が一番盛り上がっていました。

また、PMBOKを知識として知らないメンバーもおり、今後のプロジェクトマネジメントに生かしていきたいという声もあがっていました。
もちろん知識として知っているだけでは意味がありませんが、実践に移すためにはそもそも知識として知っていなければできないので、それを知るための良い時間だったと思います。

お悩み相談は20分ほどの時間だったのですが、より具体的なコミュニケーションの取り方や見積もりの方法などについても言及があり、時間が足りないほどでした。
私自身、ビジネスサイドとコミュニケーションを密に取らないといけなかったり、見積もりを出さないといけなかったりと、今現在まさにという状況だったので、すぐさま取り入れようと思います。

お悩み相談、コメント欄、アンケートなどを見ても見積もりに言及しているメンバーが多かったので、見積もり特化の第二弾も開催されるかもしれません!

最後に

定期勉強会も第3回となりましたが、あまりテーマが偏らないように開催していこうと思っています。
今後とも様々な勉強会を開催していくのでよろしくおねがいします。

JavaでHTMLの不要なスペース・改行を削除する

こんにちは。 エキサイト株式会社の三浦です。

JavaでHTMLを構築するとき、タグ間などに存在する不要なスペースや改行を消したいと思ったことはありませんか? 今回は、それらを削除する方法について説明します。

HTMLと不要なスペース・改行

例えば、以下のようなHTMLを作りたいとしましょう。

<div>
    <span>hello world!</span>
    <a href="https://sample/">sample site</a>
</div>

通常ならこれで問題ありませんが、それ以降のHTMLの整形処理などの関係で、不要なスペースや改行を削除したいと思ったことはありませんか? 上記のHTMLなら、以下のような状態にしたい、というものです。

<div><span>hello world!</span><a href="https://sample/">sample site</a></div>

ですが、例えばThymeleafなどを使っている場合、こんな形式はあまりに可読性が低いと言わざるを得ません。 PHPsmartyと呼ばれるテンプレートエンジンであればstripというタグを使うことができるのですが、Thymeleafにはそのような機能はありません。

そこで、今回はこの strip の機能を参考に、不要なスペース・改行削除処理を自作してみました。

スペース・改行削除処理

strip の機能は、

各行の先頭と終端にある 余分なホワイトスペースやキャリッジリターンが除去されます。

というものです。 つまり、

  1. 改行文字ごとにHTMLを分解し、
  2. それぞれをtrimする

事ができればいいわけです。

すなわち、以下の処理になります。

Pattern BREAK_PATTERN = Pattern.compile("\r\n|\r|\n");

String html = "整形したいHTML";
String strippedHtml = Arrays
    .stream(BREAK_PATTERN.split(html))
    .map(htmlLine -> htmlLine.trim())
    .collect(Collectors.joining());

ついでにタグ文字を取り除きたかったり、半角・全角スペースしか存在しない行は削除したいなどのカスタムな要望があれば、以下のように処理することもできます。

Pattern BREAK_PATTERN = Pattern.compile("\r\n|\r|\n");
Pattern TAB_PATTERN = Pattern.compile("\t");

String html = "整形したいHTML";
String strippedHtml = Arrays
    .stream(BREAK_PATTERN.split(html))
    .map(htmlLine -> TAB_PATTERN.matcher(htmlLine.trim()).replaceAll(""))
    .filter(htmlLine -> StringUtils.isNotBlank(htmlLine))
    .collect(Collectors.joining());

最後に

最初に書いたとおり、そもそもHTMLはこの程度のスペースや改行は気にする必要がないようにできています。 なので、Thymeleafにデフォルトで削除機能がないのは、ある意味当然と言えます。

可能であれば、そもそもこれらのスペース・改行を気にせずに済むような状態にコードを変更することを優先し、どうしようもない場合のみ今回のコードの使用を検討してみてもらえると良いかと思います。

Formでint型の変数には@NotNullは使えない

こんにちは、エキサイトのしばたにえんです。

知っている人なら当たり前のことかもしれませんが、Formでint型の変数には@NotNullは使えません

早速ですが以下をご覧ください

SampleController.java
@RestController
public class SampleController {
    @GetMapping("sample")
    void get(@ModelAttribute @Valid SampleRequest sampleRequest, BindingResult bindingResult) {
        if (bindingResult.hasErrors()) {
            throw new BadRequestException(
                    bindingResult
                            .getFieldError("num")
                            .getDefaultMessage()
            );
        }
    }
}
SampleRequest.java
@Data
public class SampleRequest {

    @NotNull
    private int num;
}

http://localhost:8080/sampleにアクセス

<Response body is empty>

numがない状態でのアクセスなので@NotNullに引っかかってエラーになると思いますが エラーは表示されません。 クエリパラメーターで値が入っていない場合 numにはint型のデフォルト値が入りnum = 0となるため @NotNullには引っかからないのです。

対処法
@Data
public class SampleRequest {

    @NotNull
    private Integer num;
}

http://localhost:8080/sampleにアクセス

{
  "error": {
    "message": "null は許可されていません"
  }
}

numにはInteger型のデフォルト値が入りnum = nullとなるため @NotNullでバリデートされています。

業種交流LT会【クリエイティブ編】を開催しました 🎉

業種交流LT会【クリエイティブ編】を開催しました
業種交流LT会【クリエイティブ編】を開催しました

エキサイトのあはれんです。

業種、部署、社歴を問わず交流できる場を創出することを目的とした業種交流LT会を開催しました 🎉

今回のテーマは「クリエイティブ」ということで、デザイナー職の方に登壇していただきました。

デザイナー職、エンジニア職、ビジネス職、またインターン生も参加してくださりました。最大参加人数は56名にも達し、多くのメンバーが参加してくれました。😊🎉

登壇内容

デザイナー職の方に、どんな仕事をされているのかを説明していただきました。

トンマナ作成のお話

トンマナ作成のお話
トンマナ作成の話

6月にリリースされたリモート占いサービス「Maja(マーヤ)」でデザイナーをされている高橋さんに、 トンマナについて話していだきました。

トンマナとは...

「トーン&マナー」の略でデザインやトーンの一貫性を保つための"ルール"として使われる。

Maja(マーヤ)」では、チーム総出でサービスのミッション、ビジョン、バリューを考え、 それをもとにトンマナを決めたことで、迷いが発生せず、デザインがしやすかったそうです。

私はエンジニアとして「Maja(マーヤ)」に参加していましたが、トンマナ決めは、サービス理解をより深めてくれる機会になってよかったです。😊

maja.excite.co.jp

エンジニアよ UIについてもっと対話してくれ!

エンジニアよ UIについてもっと対話してくれ!
エンジニアよ UIについてもっと対話してくれ!

6月にリリースされた経営管理プラットフォーム「KUROTEN.(クロテン)」でデザイナーをされている新野さんに、 チームでやっているエンジニアとデザイナーとの対話の取り組みについて話していただきました。

デザイナーが作ったデザインとエンジニアがフレームワークを利用して実装したものに差分が出てしまう課題に対して、 デザインシステム勉強会の開催やデザインシステムを導入することで、解決に取り組んだそうです。

この発表を聞き、デザイナーの意図をまだまだ汲み取れていないと反省しました...。😨

デザイナー職ともっと対話を増やしてよりよいUXを目指したいと思いました!💪😤

lp.kuroten.jp

最近やっている仕事

最近やっている仕事
最近やっている仕事

21卒デザイナーの山﨑さんに、最近の業務に関して話していただきました。

記事クリエイティブ制作やTシャツのデザイン制作を行っているそうです。 制作過程について説明していく中でイメージボード制作について以下のように話してくださりました。

ターゲット選定・要件定義などビジネス側と協力して決定した後、その成果物に合ったテイストの選定のためイメージボードを製作します。 ただ「可愛い」という言葉だけでも色々な種類の「可愛い」があるので、フェミニンな可愛さ・ガーリーな可愛さ・オーガニックな可愛さなどに分類分けしたイメージボードを提出して解像度を上げていきます

なんとなくの「可愛い」ではなく分類し解像度を上げてデザインしているので、 デザインさんはユーザに対して適切な「可愛い」を伝えられるのですね。😊

配属されて3ヶ月でUIについて学んだこと

配属されて3ヶ月でUIについて学んだこと
配属されて3ヶ月でUIについて学んだこと

21卒デザイナーの鍜治本さんに、UIについて学んだことを話していただきました。

鍜治本さんも「KUROTEN.(クロテン)」のデザイナーさんでして、 「KUROTEN.(クロテン)」でのFigmaの活用方法について話していただきました。

紹介していただいたFigma運用方法が良かったので、 私のチームでもFigmaをより活用できるようにFigmaについてもっと勉強したいと思いました!

まとめ

業種、部署、社歴を問わず交流できる場を創出することを目的とし、開催したLT会でしたが、 アンケートで以下のような声があり、交流のきっかけをつくることができたのではないかと思いました。

  • 他の業種の方のデザイン(UIなど)に対する愛や熱意あふれるプレゼンを聞いて、その領域に関して理解が深まりました。また、今後自分のエンジニアとしてのキャリアでも役に立ちそうな知識も多く得られたので良かったです。
  • デザイナーと開発の対話が必要とおっしゃっていて、今回のLT会もエンジニアがデザイナーを理解する対話の一つだなと思いました!
  • 他事業・他部署のデザイナーがどんなふうに仕事をしているか見れて楽しかったです!

今後もLT会のレポートを紹介していきますので見ていただけると幸いです。

最後に

エキサイトでは一緒に働いてくれる仲間を絶賛募集しております!

長期インターン、夏季インターンも歓迎していますので、興味があれば連絡いただければと思います🙇‍♀️

www.wantedly.com

JUnitのテストでDisplayNameを使い、わかりやすいテストを書こう

こんにちは。 エキサイト株式会社の三浦です。

皆さん、JUnitユニットテストを書いていますか? 今回は、@DisplayNameを使うことによってJUnitユニットテストの可読性が上がるという話です。

ユニットテストの可読性の問題点

ユニットテストは、テスト対象が複雑になるほどコード量が多くなっていきます。 それは、検証するパターンが多かったり、テストのために用意するモデルやモックが増えていくためですが、その結果各ユニットテストのメソッドが何のテストなのか、ぱっと見ただけではわからなくなることがあります。

今回はそれを、DisplayNameというアノテーションを使うことで解決していきます。

DisplayNameの使い方と効果

DisplayNameの使い方は簡単で、テストメソッドにアノテーションとしてつけるだけです。

@ExtendWith(MockitoExtension.class)
public class SampleTest {

    @Test
    @DisplayName("サンプルテスト")
    void sample() {
        Assertions.assertEquals(
                true,
                true,
                "必ず成功する"
        );
    }
}

好きな文字列でテストメソッドに名前をつけることができるようになり、可読性が上がったかと思います。 また、テストに失敗した場合、

@ExtendWith(MockitoExtension.class)
public class SampleTest {

    @Test
    @DisplayName("サンプルテスト")
    void sample() {
        Assertions.assertEquals(
                true,
                false,
                "必ず成功する"
        );
    }
}

以下のようにログを表示してくれるため、失敗したメソッドを探し出すのも簡単になります。

SampleTest > サンプルテスト FAILED

最後に

ちょっとしたことですが、ぱっと見ただけで何をしているかわかるというのは非常に良いことです。 ぜひ使ってみてください。

ボイス&トーンについて理解が深まった話

f:id:designsazuka:20210816115124p:plain
ボイス&トーンについて理解が深まった話

こんにちは!課金サービスのデザイナーのSAZUKAです。

技術書より推理小説のほうが好きなんですが、先日読んだ「UXライティングの教科書」という本がおもしろかったので紹介します。

UXライティングとは

「UXライティング」という言葉自体あまり浸透していないと思います。読んで字の如くですが一言で説明するとこんな感じ。

「言葉や文章によるユーザーに与える様々な経験」

UXデザイン=デザインによる経験、のように安易に理解されがちなんですが、実際はUXデザインはもっと広くて深い。カスタマージャーニーマップもユーザーアンケートもペルソナ作成なんかもUXをより良くするための手法です( ˘ω˘ )👆

だからUXライティングはUXデザインの中の1つだと考えるのが良さそう。

ボイス&トーン

参考書で何度も登場する「ボイス&トーン」という言葉があります。

  • ボイス:私たちの声が一人一人異なるのと同じように、ブランドやサービスも個性として一貫性を持つこと。

  • トーン:機械的ではなく人間味を持ったコミュニケーション、話し方。

いわゆるブランドやサービス自体の「ペルソナ」なのかなと思います。

いちばん腑に落ちたのが、オフラインで心のこもった接客を受ける場合、webサイトを訪れたときも同じように対人接客のようなおもてなしをするべきではという考え。

旅行先のホテルの接客はとても素晴らしいのに、申し込みサイトで全く歓迎する雰囲気のないページに無機質で機械的なテキストばかり並んでいたらギャップが生まれます。逆に予約時のボイス&トーンを高めれば満足度もアップします。

「一貫性」は重要である

紹介されていたIKEAはオフラインもオンラインも同じユニークさを感じます。私自身も足を運んだことがありますが、オンラインショップとのギャップや違和感を全く感じないほど一貫されていると思いました。

例えば節水アイテムの紹介ページは「歯を磨いているときは蛇口を閉めましょう。「ちりも積もれば山となる」です。」いうテキストが書いてありますが、もしこれが「このシリーズの蛇口は節水できます」になっていたら全くIKEAらしさはなくなってしまいますよね( ˘ω˘ ;)💦

自分が今デザインを担当している「エキサイトお悩み相談室」(カウンセラーに電話相談できるサービス)なら、たとえば登録画面では「一歩踏み出してみませんか」「カウンセラーに話してみましょう」などの言葉を一言添えるべきなのかもしれません。

自分がユーザーだったら(サイトに来たとき)どんな言葉をかけてほしいか、これは忘れてはならないので念頭に置いておきます( ˘ω˘ )👆

ボタンにはユーザーがやること(行動)ではなく、ユーザーが得るもの(価値)を伝える言葉を入れるべき

自分が関わっているデザインで1つ紹介。昨年の話ですが、当時エキサイトお悩み相談室のカウンセラー詳細ページには「相談受付中」ボタンというものがありました。機能としては、カウンセラーが待機していていつでも相談可能な状態を表すボタンです。

f:id:designsazuka:20210816113707p:plain
カウンセラー詳細の改修前と改修後

これはステータスではないのか?ステータスをボタンにして良いのか?とジョインしてからずっと違和感を抱いており、チームで話し合いの末「いますぐ電話相談する」に変え、ステータスは上部に持ってきました。

結果はものすごい成果が出たわけではありませんが、悪くないものでした(ここでは数字は割愛させていただきます)

旧デザインの「相談受付中」というのは私たちサービス側の都合であり「今受付てますよ〜!」と店の前で呼び込みをしているような状態です。

一方、新デザインは「いますぐ相談したい」というユーザーの意思をそのままボタンにしています。(「カウンセラーに話を聞いてもらう」という言葉も良さそうですね。長いけど。)

また、旧デザインは「相談受付中か…ということは相談できるということね!」とユーザーに考えるという作業を与えてしまうので直感的ではありません。

ユーザーが「そのとき何をしたいのか」「何を求めているのか」をWHYし続けると、意外と拍子抜けするくらいシンプルな言葉に辿り着くかもしれません。

他にも紹介したい事例はありますが長くなってしまったのでまた今度書きます。

最後に

余談ですが、この前「UXライティングの教科書」をベースにしたオンラインセミナーに参加しましたが参加者が200名超えで驚きでした。日本でも注目されつつあるけどまだ浸透していないので新しく勉強するのもオススメです👌

OSSコミッターになってみよう

こんにちは、エキサイト株式会社エンジニアの伊藤(🐦 @motokiito2)です🙌

みなさんはOSSへのコミットに対して、どのようなイメージを持ちますか?

「つよつよなエンジニアがよくやってる!」

「敷居が高すぎるから自分には関係ない。。」

なんて思う方も、少なくないと思います。(自分も以前はこう思っていました)

ですが、この認識は間違っていて、きっかけさえあれば誰でもOSSコミッターになれるのです!

今回は、実際に私が最近OSSにコミットしたケースと流れを紹介したいと思います!

修正箇所を発見する

私は、業務で PHPフレームワーク Laravel を利用しています。

あるとき、いつものようにコードに対して静的解析を実行したところ、phpstanで未到達コードの警告が発生しました。

警告が発生したのは、以下のようなコードでした

        if (is_null($this->request->old())) {
            return false';
        }

return false; 行には到達しないとの警告でしたが、デバッガでは確かに到達するので、どういうことかと調べたところ、

Laravel のコード src/Illuminate/Http/Concerns/InteractsWithFlashData.phpold() は、下記のような実装になっていました (Laravel 現行バージョンでは修正済みです)

    /**
     * Retrieve an old input item.
     *
     * @param  string|null  $key
     * @param  string|array|null  $default
     * @return string|array
     */
    public function old($key = null, $default = null)
    {
        return $this->hasSession() ? $this->session()->getOldInput($key, $default) : $default;
    }

お気づきでしょうか。

$this->hasSession()false かつ、$default 引数が与えられていない場合にnull を返すことがあるにも関わらず、phpdocの表記は、@return string|array となっていますね。

要はphpdocの記述が間違っているだけなのですが、このレベルの間違いは、Laravelに限らずOSSではよくあることです。

気になる方は、自分がよく利用しているOSSリポジトリを探して、コミットログを見てみてください!

修正する

修正箇所がわかったので、修正してみましょう!

(このあたりの手順はOSSによって異なる場合があるので、READMEやContribution Guideを読んでみてください。)

修正を行うために、まずはLaravelのリポジトリをforkします。

f:id:itomoto0312:20210810181358p:plain
fork laravel

forkしたリポジトリをローカルにcloneしたら、修正用のブランチを切ります。

Laravelの場合は、Contribution Guideに

master ではなく current LTS branch からブランチを切ってね!」

と注意書きがありました。

OSSによってルールが設定されているので、よく読んでおきましょう。

ルールに従っていないと、せっかくPRを出してもmergeされずcloseされてしまいます。

ブランチ名については、Laravelの場合は特に指定がなかったので、過去にマージされていたブランチ名を参考に fixDocInInteractsWithFlashData としました。

さて、今回の修正は非常に簡単で、該当箇所のphpdocの記述を修正するだけです。

    /**
     * Retrieve an old input item.
     *
     * @param  string|null  $key
     * @param  string|array|null  $default
     * @return string|array|null
     */
    public function old($key = null, $default = null)
    {
        return $this->hasSession() ? $this->session()->getOldInput($key, $default) : $default;
    }

修正が終わったので、コミットしてpushします。

コミットメッセージも、特にルールが設定されていなければ、過去にマージされたコミットを参考にすると良いと思います!

PRを出す

修正が終わったので、次はプルリクエストを出します。

プルリクエストの記述についてもルールが設定されていることがあるので、事前に確認しておきましょう。

今回は、Laravel の Contribution Guide に従い、merge先を current LTSである laravel:8.x に設定し、フォーマットに沿って修正内容等を記述しました。

実際のプルリクエストはこちらです。

github.com

ここまでできたら、後はプルリクエストが承認されることを祈りながら待ちます。

今回の場合は、修正内容が簡単なこともあり、半日ほどでmergeされました!

あとがき

今回は、実際に私がLaravelにコミットした際の流れをご紹介しました。

今回のような簡単な修正でも、OSSに貢献することができます。

このケースを知って、OSSコミットへの敷居が下がった方もいらっしゃるのではないでしょうか!

「自分でもできそう!やってみよう!」と思う方が一人でもいてくれたら嬉しいです👍

本当にあった、何もしていないのに突然DBの負荷が爆増した話

こんにちは。 エキサイト株式会社の三浦です。

「何もしていないのに壊れた」という経験、皆さんはあるでしょうか? 今回はそのパターンの1つ、本当に私が体験した「何もしていないのに突然DBの負荷が爆増した話」をさせていただきます。

始まり

その日は、大きなリリースもユーザーの急激なアクセスもなく、特に何の変哲もない日でした。 いつもどおり開発をしていると、ふとSlackに、サービスで使用しているDB(RDS)のCPU使用率が高くなっているとの通知が届いていることに気づきました。

(あー、なんか急激にアクセスが来たか何かなのかな…)と思いながらAWSコンソールを開いて確認してみると…なんと、CPU使用率が100%に張り付いているのです。

一気に背筋が凍り、すぐにメンバーに連絡しつつ、調査に入りました。

調査

改めてAWSコンソールを見ると、ある時間から急にCPU使用率が急上昇し、そのまま100%まで行ってしまっているようです。 それまではせいぜい30 ~ 40%程度ですし、今までそんなことは起きていないので、明らかに異常事態でした。

とりあえずその時間帯に何かの新規リリースがあるか調べてみると…ありました。 ちょうど、DBを触るAPIの機能改修のリリースです。

ですが、コードを見る限りDBの負荷が上がるような変更ではありません。 メンバーと連携しつつ、祈るような気持ちで前バージョンに戻してみましたが、結果は残念ながら効果なしでした。

それと並行し、急激にアクセスが増えていないか、ユーザー全体だけでなくBotのアクセスだけが急激に増えていたりしないかなども調べてみましたが、全く変化している様子はありません。

更に、(急にスロークエリが出てきたともあまり思えませんが)スリークエリログを調べてみても、特段ヤバそうなものは見当たりません。

この時点で、この障害対応が長期戦になることを覚悟しました。

光明

それは、メンバーのメンタルがやられてきた時のことでした。 あるメンバーが一言、「プロセスリストはどうなっているんだろう?」と言ったのです。

プロセスリストとは、現在DBで実行されているプロセス一覧です。 確かに見ていないなと思い確認してみると、なんと10分以上に渡って実行され続けているプロセスが複数個あるではありませんか!

そのSQLをもとにどこからのリクエストか調べてみると、どうやらとあるバッチからのアクセスのようです。 実際にそのSQLを手動で流してみると、とても重いSQLであることが判明しました。 Explainを調べて見ると、Indexこそ当たっているものの、使用するIndexを手動で選択していたせいもありIndexの当たり方に問題があって、実行時間としては遅くなってしまっていたようでした。

取り急ぎバッチサーバを停止し、プロセスリストから手動で該当のプロセスを削除して見たところ、みるみるCPU使用率が下がっていき、ようやくこの騒動の終わりが見えたのでした。

原因

バッチサーバの再開、SQLの修正などの騒動の後始末の中、メンバーで今回の原因が何だったか話し合っていたのですが、「おそらくはレコードの増加が原因だろう」という結論に帰着しました。 今回のテーブルはバッチ等で自動的にレコード数が増えていくタイプのテーブルだったのですが、レコード件数がある一定値を超えてしまった結果、SQLが全く同じにもかかわらず重くなり、結果として今回の騒動に至ったというわけです。

まさしく(コードの修正という意味では)何もしていないのに、突然DBの負荷が爆増したわけでした。

最後に

今回のように、自動的にレコード数が増えるタイプのテーブルでは、何もしていなくても状況が急変する可能性があります。

そういった際、「新規リリース」や「アクセスの増加」などの外的要因が原因だと決めつけるのではなく、もしかしたらレコードの増加のような、一見見えにくい内的要因が原因の可能性もあると考えておくだけでも障害対応の速度が上がるのでは無いでしょうか。 皆さんの参考になれば幸いです。

なお今回は、RDSであることを最大限利用し、動的にReadインスタンス数を増やし、余剰のインスタンスを持った上でスペック(インスタンスタイプ)の変更や再起動をすることができました。 結果として、幸いにもユーザーから見る上ではサービスがそこまでDBの障害の影響を受けることはなく、大事に至らなかったので、そこだけは不幸中の幸いでした。

GrafanaとPostgreSQLをDocker Composeで動かしたときに接続先の設定でハマった

はじめに

エキサイト株式会社 21卒 バックエンドエンジニアの山縣です。 連休中にGrafana + PostgreSQLをDocker Composeで動かしたときになかなか連携できずにハマってしまいました。 そのときに起きた問題点と解決策を共有します!

導入

今回使用したdocker-compose.ymlファイルです。 GrafanaとPostgreSQLを使用しています。

version: '3'
services:
  grafana:
    image: grafana/grafana
    container_name: sample_grafana
    ports:
      - 8888:3000
    volumes:
      - ./grafana:/var/lib/grafana

  postgres:
      image: postgres:12.7
      container_name: sample_postgres
      ports:
        - 5432:5432
      environment:
        POSTGRES_USER: sample_user
        POSTGRES_PASSWORD: sample_pass
      volumes:
        - ./initdb:/docker-entrypoint-initdb.d
        - ./postgres:/var/lib/postgresql/data

GrafanaのConfiguration > Data Sources > Add data sourceから接続先の設定をすることができます。 その際、ホストをlocalhost:5432で設定すると接続することができませんでした。

結論

f:id:excite-kazuki:20210805231418p:plain

docker-compose.ymlにあるcontainer_nameをホストに記述する必要があります。 Dockerコンテナを使用している場合は、localhostのままでは繋がらないため注意しなくてはなりません。

上記のとおり接続先の設定ができると、PostgreSQLに格納したデータを可視化することができるようになります!

f:id:excite-kazuki:20210806004454p:plain
Grafanaでメモリ使用量を可視化

Djangoで複数アプリケーション構成にするときのTips

iXIT株式会社 小長谷です。

DjangoはWebアプリの各種設定情報を管理するための仕組みとして、「プロジェクト」を使用します。さらに「プロジェクト」内で、Webアプリのためのモジュールを作成します。

1つのアプリケーションに対し1つのフォルダが作成されるため、複数のアプリケーションを作成する場合管理がしづらいと考え、新たにappsフォルダを作成してまとめる構成で開発を行いました。
また、ビュー用のtemplateファイルもtemplatesフォルダに、その他静的ファイルもstaticフォルダにまとめました。

開発時にsettings.pyに追加するものなど、つまずいた箇所を記載していきます。

バージョン

Python: 3.9.6
Django: 3.2.5

プロジェクト、アプリケーションを作成

プロジェクト名を仮に、bookProjectという名前にします。
django-admin startproject bookProject
cd bookProject
startappコマンドでフォルダを指定する場合、予めフォルダを作成する必要があります。
mkdir -p apps/book
python manage.py startapp book apps/book
アプリケーションを追加する場合、上記のようにapps配下に作成していきます。

フォルダ構成図

templatesフォルダ、staticフォルダを作成した後の全体図は以下のようになります。

├── bookProject
│   ├── __init__.py
│   ├── __pycache__
│   ├── asgi.py
│   ├── settings.py
│   ├── urls.py
│   └── wsgi.py
├── apps
│   ├── book
│       ├── __init__.py
│       ├── __pycache__
│       ├── admin.py
│       ├── apps.py
│       ├── migrations
│       ├── models.py
│       ├── tests.py
│       └── views.py
├── db.sqlite3
├── manage.py
├── static
│   └── css
│       └── base.css
└── templates
    ├── base.html
    └── book
        └── index.html

settings.py

bookProject/settings.py

# 追加
import os

# ~~ 途中省略 ~~

INSTALLED_APPS = [
    'django.contrib.admin',
    'django.contrib.auth',
    'django.contrib.contenttypes',
    'django.contrib.sessions',
    'django.contrib.messages',
    'django.contrib.staticfiles',
    # 追加
    'apps',
]

# ~~ 途中省略 ~~

TEMPLATES = [
    {
        'BACKEND': 'django.template.backends.django.DjangoTemplates',
        'DIRS': [
            # 追加
            os.path.join(BASE_DIR, 'templates'),
        ],
        'APP_DIRS': True,
        'OPTIONS': {
            'context_processors': [
                'django.template.context_processors.debug',
                'django.template.context_processors.request',
                'django.contrib.auth.context_processors.auth',
                'django.contrib.messages.context_processors.messages',
            ],
        },
    },
]

# ~~ 途中省略 ~~

STATIC_URL = '/static/'
# 追加
STATICFILES_DIRS = (os.path.join(BASE_DIR, 'static'),)

ルーティング

bookProject/urls.py

from django.contrib import admin
from django.urls import path, include

urlpatterns = [
    path('admin/', admin.site.urls),
    path('book/', include('apps.book.urls')),
]

bookフォルダ内にurls.pyを作成します。
apps/book/urls.py

from django.urls import path
from . import views

app_name = 'book'

urlpatterns = [
    path('', views.IndexView.as_view(), name='book_index'),
]

ビュー

apps/book/views.py

from django.shortcuts import render
from django.views.generic import TemplateView


class IndexView(TemplateView):
    template_name = 'book/index.html'

テンプレート

templates/base.html

{% load static %}

<!-- 省略 -->

<link rel="stylesheet" href={% static 'css/base.css' %}>

templates/book/index.html

{% extends 'base.html' %}

<!-- 省略 -->

<a href={% url 'book:book_index' %}>bookへ</a>

javaのSpringBootでパラメーターの日付をLocalDateTimeで受け取る方法

エキサイトのエンジニア藤沼です。 JavaのSpringBootにて、Getパラメーターで日付をLocalDateTimeで受け取る方法の覚書です。

やりたい事

下記のようなAPIにて、日付パラメータをLocalDateTimeで受け取りたい

/api/sample/?start_date_to=20201010000000

何故か取れない

@GetMapping("sample")
public Mono<LocalDateTime> sample(
        @ModelAttribute @Validated SampleForm sampleForm, BindingResult bindingResult
) {
    return Mono.defer(
            () -> {
                return Mono.just(sampleForm.getStartDateTo());
            }
    ).subscribeOn(Schedulers.boundedElastic());
}
@Data
public class SampleForm {

    /**
     * 開始日時to
     */
    private LocalDateTime startDateTo;

    @ConstructorProperties({
            "start_date_to"
    })
    public SampleForm(
        LocalDateTime startDateTo

    ) {
        this.startDateTo = startDateTo;
    }
}

実行しても何故か日付はnullになる

/api/sample/?start_date_to=20201010000000

f:id:aya_excite:20210804155117p:plain

※捕捉 ConstructorPropertiesは、 getメソッドでパラメーターをどう受け取るかを指定できます。

LocalDateTimeで受け取るには@DateTimeFormatが必要だった

@GetMapping("sample")
public Mono<SampleListResponseModel> sample(
    @ModelAttribute @Validated SampleListForm sampleListForm
) {
@Data
public class SampleListForm {

    /**
     * 開始日時to
     */
    private LocalDateTime startDateTo;

    @ConstructorProperties({
        "start_date_to"
    })
    public SeriesListForm(
        //↓このアノテーションをつける
        @DateTimeFormat(pattern = "yyyyMMddHHmmss")
        LocalDateTime startDateTo

    ) {
        this.startDateTo = startDateTo;
    }
}

日付が受け取れるようになった

/api/sample/?start_date_to=20201010000000

f:id:aya_excite:20210804155755p:plain

捕捉:一つずつパラメーターを受け取る場合
@GetMapping("sample")
public Mono<SeriesListResponseModel> sample(
    @RequestParam("start_date")
    @DateTimeFormat(pattern = "yyyyMMddHHmmss")
    LocalDateTime startDate
) {

RDSのパラメータグループが「デフォルト」に戻せない原因?

こんにちは。 エキサイト株式会社の三浦です。

みなさん、Amazon RDSは使っているでしょうか? RDSでは「パラメータグループ」という機能を使ってDBのパラメータを変更するのですが、今回はそのパラメータグループに関するちょっとしたTipsを書いていきます。

Amazon RDS とは

Amazon RDS は、AWSが提供しているマネージドなDBサービスです。

Amazon Relational Database Service (Amazon RDS) を使用すると、クラウド上のリレーショナルデータベースのセットアップ、オペレーション、スケールが簡単になります。ハードウェアのプロビジョニング、データベースのセットアップ、パッチ適用、バックアップなどの時間がかかる管理タスクを自動化しながら、コスト効率とサイズ変更可能なキャパシティーを提供します。

DBである以上 sort_buffer_size などの細かいパラメータも変更できる必要があるのですが、これはRDS内の「パラメータグループ」という機能によって提供されています。

f:id:excite-takayuki-miura:20210802112656p:plain
RDSのパラメータグループ

各種パラメータには最初はデフォルト値が設定されていて、このパラメータグループ上で値を変更することで変更値をDBに反映させることができます。

パラメータに変更を加えた後でもデフォルト値に戻すことができるのですが、実はたまに戻らないことがあります。

デフォルト値に戻らない原因?

ある日、パフォーマンステストのためにいくつかのパラメータを変更し、テストが終わった後にデフォルト値に戻そうとしたら一部パラメータが戻らない事がありました。 Webコンソール上ではエラーが起きている様子はなく、DB自体にもエラーは見受けられないのですが、パラメータはデフォルト値に戻っていないのです。

その後、色々な検証をしていく中で一つの可能性に行き当たりました。

RDSにはステータスがあり、通常は「利用可能」となっています。

f:id:excite-takayuki-miura:20210802113545p:plain
ステータス

パラメータグループでパラメータを変更すると、これが変更中を示す状態になるのですが、どうやら変更中にデフォルト値に戻そうとすると正しく戻ってくれないようです。

一見当然のことのように思えるのですが、

  • パラメータを変更した画面からはすぐにはDBのステータスが見られない
  • デフォルト値に戻すのに失敗しても、戻っていないのは確認できるが原因は表示されない
  • 場合によっては、パラメータを変更しても変更中を示す表示にならないことがある?
    • 変更自体は終わっているが、裏側でまだ副次的な処理が続いているとか?

ために見落とすことが意外と多いのではないでしょうか。 こういった場合は、しばらく時間をおいて再度実行してみると良いでしょう。

最後に

今回書いた理由以外にも、もしかしたら戻らない理由があるかもしれないのでご了承ください。

なお、もしTerraformで管理している場合は、Terraformから変更すればそのあたりも込み込みで変更してくれるのか、こういったことを気にすることなくデフォルト値に戻ります。 限定的な場面ですが、

  1. Terraformで管理しているRDSについて
  2. テストのために手動でパラメータグループをデフォルト値から変更して
  3. いざ手動で戻そうとしたら戻らなかった

場合は、Terraformから戻してみるのも手かと思います。

length()の値が違う

エキサイト株式会社 メディアサービスエンジニアのしばたにえんです。
早速以下のコードを見てください

    void getStringLength() {
        String str = "𩸽";
        System.out.println(str.length());
    }

実はこの結果は2です。 原因はサロゲートペアにあります。

サロゲートペアとは

通常、Unicodeでは1文字あたり2バイトのデータ量を使います。 2バイトですから65536通り(0x0000~0xFFFF)のビットを表現できます。 この約6万字で世界中の文字を表現しようというのがUnicodeの本来の思想でした。 ところが、近年、Unicodeに組み込みたいという文字の要望がいろいろと増えてきました。 結果的に従来の2バイト(65536文字)では文字が足りない状況になってしまったのです。 そこで、解決策としてサロゲートペアという方法が導入されました。 「1文字=2バイト」の基本は維持しつつ、一部の文字については「1文字=4バイト」にする方法です。

    void getCharLength() {
        String str = "𩸽";
        System.out.println(Integer.toHexString(str.charAt(0)));
        System.out.println(Integer.toHexString(str.charAt(1)));
    }
// 実行結果
d867
de3d

"𩸽"は見た目上は1文字ですが、サロゲートペア文字で4バイトで表した文字でchar2つで表されます。length()はchar1つにつき1とカウントされるため値が違ったのです。

本当の文字数を取得する

    void getRealLength() {
        String str = "𩸽";
        System.out.println(str.codePointCount(0, str.length()));
    }
// 実行結果
1

codePointCountメソッドでは正しく1とカウントできていることが確認できます。 codePointCountメソッドでは、文字列をカウントする際の開始/終了位置が必須なので、文字列全体をカウントするには、それぞれ0(先頭)、str.length(末尾)を指定しています。

length()を使う際は気をつけましょう。

Nim言語を使って簡単に文章の類似度を計算してみる

皆さんは「Nim」という言語をご存知でしょうか?
Nimは「Pythonをブラッシュアップして、秘伝の悪魔のタレをかけたような感じ」と比喩されるような言語です。
そしてNimはC言語などにコンパイルされ、C言語などのコンパイラを使ってバイナリにコンパイルされます。
そんなNim言語を触っていきましょうか。

Nim言語について

公式サイトは以下です。
https://nim-lang.org/

文法はPythonに影響を受けており、インデントが意味を持ついわゆる「オフサイドルール」というものを採用しています。
また静的型付けです。

Hello World

とてもシンプルです。

echo "Hello World"

コンパイル

こちらも簡単です。
以下のような形になります。

$ nim c -r 保存ファイル名

nim cで「C言語コンパイル」を意味します。
-rコンパイル後即実行です。これを省略すると、バイナリコンパイルだけが行われます。

蛇足ですが、nim cC言語に、
nim cppC++に、
nim objcObjective-Cに、
nim jsJavaScriptコンパイルされます。

遊んで見る

少し自然言語処理のようなことをして遊んでみましょうか。
n-gramを自作してみましょう。

自然言語におけるn-gram

n-gramのnは変数です。
例えば、uni-gram、bi-gram、tri-gram、4-gram…のような感じになります。

例を見てみましょう。
例文:「今日はいい天気」

uni-gram

uni-gramでは以下のように分解されます。

今
日
は
い
い
天
気

bi-gram

bi-gramでは以下のように分解されます。

今日
日は
はい
いい
い天
天気

このような感じになります。

n-gramを作成するプログラムを書いてみる

以下のコードをtest.nimとして保存します。

import unicode

proc createNGram*(n: int, text: string): seq[string] =
    ##
    ## n-gramデータを作成します
    ## 
    ## n: n-gramのnに当たる数値
    ## text: n-gramに分解(コーパス)したい文字列
    
    # マルチバイト文字列を扱えるようにテキストをルーン化する(Goとかにもある、アレ)
    let runeText = text.toRunes()

    # 何文字目まで連結させたかを保持しておく変数(Runeは1文字ずつ扱うため、indexでどこまで扱ったかをカウントしておく)
    var index = 0
    # n-gramで何文字ずつの文字列(コーパス)にするかを決めるためのカウント変数
    var cnt = 0
    # n-gramでの文字列を作成する際に利用するtmp変数
    var tmp: string

    while true:
        # cntがn-gramの指定文字数を超えたらそこで切り出す(安全のため<=としている)
        if n <= cnt:
            # resultは暗黙変数(nimでは返り値を定義すると自動的にresult変数が生成される)
            result.add(tmp)

            tmp = ""
            cnt = 0

            # n-gramの特性上一つ前の文字をもう一度利用するので、n-1をしている
            index = index - (n - 1)
            
        
        if text.runeLen() <= index:
            break
        
        # 1文字ずつ連結していく
        tmp = tmp & $runeText[index]

        cnt = cnt + 1
        index = index + 1

echo createNGram(2, "今日はいい天気")

コマンドプロンプトやターミナルで以下のコンパイルコマンドを実行すると、
「今日はいい天気」という文字列がn-gramに分解されて表示されると思います。

$ nim c -r test.nim

@["今日", "日は", "はい", "いい", "い天", "天気"]

いい感じですね!
コメントにも書きましたが、このように分割したものを「コーパス」と呼んだりします。

さて、続きはこのコーパスをどのように利用していくかを見ていきましょう。

TF

Term Frequencyといえばなんかかっこよさげですが、意味は「その単語の出現回数」です。
「今日」という単語が何回出てきた?「天気」という単語が何回出てきた?
そんな意味です。

ではNimでそれを計算してみましょうか。 test.nimを書き換えます。

import unicode
import tables

proc createNGram*(n: int, text: string): seq[string] =
    ##
    ## n-gramデータを作成します
    ## 
    ## n: n-gramのnに当たる数値
    ## text: n-gramに分解(コーパス)したい文字列
    
    # マルチバイト文字列を扱えるようにテキストをルーン化する(Goとかにもある、アレ)
    let runeText = text.toRunes()

    # 何文字目まで連結させたかを保持しておく変数(Runeは1文字ずつ扱うため、indexでどこまで扱ったかをカウントしておく)
    var index = 0
    # n-gramで何文字ずつの文字列(コーパス)にするかを決めるためのカウント変数
    var cnt = 0
    # n-gramでの文字列を作成する際に利用するtmp変数
    var tmp: string

    while true:
        # cntがn-gramの指定文字数を超えたらそこで切り出す(安全のため<=としている)
        if n <= cnt:
            # resultは暗黙変数(nimでは返り値を定義すると自動的にresult変数が生成される)
            result.add(tmp)

            tmp = ""
            cnt = 0

            # n-gramの特性上一つ前の文字をもう一度利用するので、n-1をしている
            index = index - (n - 1)
            
        
        if text.runeLen() <= index:
            break
        
        # 1文字ずつ連結していく
        tmp = tmp & $runeText[index]

        cnt = cnt + 1
        index = index + 1


proc tf*(corpus: seq[string]): Table[string, int] =
    ##
    ## コーパスの中のTFを計算します
    ## 
    ## corpus: コーパスが格納されたseq配列を指定します

    for c in corpus:
        # 連想配列にその単語があれば1加算、なければその連想配列のキーを作成し、1を代入
        if result.hasKey(c):
            result[c] += 1
        else:
            result[c] = 1


# コーパスを取得
let corpus = createNGram(2, "今日はいい天気")

# TF値を計算
let tfTable = tf(corpus)
echo tfTable

これを先ほどと同じようにnim c -r test.nimコンパイルすると以下のような出力を得ることができます。

{"いい": 1, "はい": 1, "い天": 1, "日は": 1, "天気": 1, "今日": 1}

コーパスの出現回数が取得できました。
全部出現回数は1ですね。
そして順番は担保されていませんが、今回は順番は関係ないのでこのまま進めます。

文章の類似度を求める

2つの文章の類似度を求めてみましょう。
ここではメジャーなコサイン類似度を利用してみましょうか。
コサイン類似度の他にも、ユークリッド距離、マンハッタン距離などでも求めることができます。

コサイン類似度とは?

ざっくり説明します。本当にざっくりと。

コサイン類似度はその名の通りコサインを利用します。
コサインは、角度が0度に近づけば1、90度に近づけば0になるという特性があります。

要は2つの要素が近ければ(似ていれば)1に近づき、
離れていれば(似ていなければ)0に近づくといえます。

詳しくは数学系のサイトを見てみると良いでしょう。

コードを改変

以下の様にコードを改変していきます。

import unicode
import tables
import math

proc createNGram*(n: int, text: string): seq[string] =
    ##
    ## n-gramデータを作成します
    ## 
    ## n: n-gramのnに当たる数値
    ## text: n-gramに分解(コーパス)したい文字列
    
    # マルチバイト文字列を扱えるようにテキストをルーン化する(Goとかにもある、アレ)
    let runeText = text.toRunes()

    # 何文字目まで連結させたかを保持しておく変数(Runeは1文字ずつ扱うため、indexでどこまで扱ったかをカウントしておく)
    var index = 0
    # n-gramで何文字ずつの文字列(コーパス)にするかを決めるためのカウント変数
    var cnt = 0
    # n-gramでの文字列を作成する際に利用するtmp変数
    var tmp: string

    while true:
        # cntがn-gramの指定文字数を超えたらそこで切り出す(安全のため<=としている)
        if n <= cnt:
            # resultは暗黙変数(nimでは返り値を定義すると自動的にresult変数が生成される)
            result.add(tmp)

            tmp = ""
            cnt = 0

            # n-gramの特性上一つ前の文字をもう一度利用するので、n-1をしている
            index = index - (n - 1)
            
        
        if text.runeLen() <= index:
            break
        
        # 1文字ずつ連結していく
        tmp = tmp & $runeText[index]

        cnt = cnt + 1
        index = index + 1


proc tf*(corpus: seq[string]): Table[string, int] =
    ##
    ## コーパスの中のTFを計算します
    ## 
    ## corpus: コーパスが格納されたseq配列を指定します

    for c in corpus:
        # 連想配列にその単語があれば1加算、なければその連想配列のキーを作成し、1を代入
        if result.hasKey(c):
            result[c] += 1
        else:
            result[c] = 1

proc cosineSimilarity*(text1: string, text2: string, ngramNum: int): float =
    ##
    ## 文章の類似度を調べます
    ## 
    ## text1: 1つ目の文章
    ## text2: 2つ目の文章
    ## ngramNum: 何gramにテキストを分解するか
    ##

    # 文章をそれぞれコーパスに分解します
    let text1Copus = createNGram(ngramNum, text1)
    let text2Copus = createNGram(ngramNum, text2)

    # text2のTF値を求めます
    let text2Tf = tf(text2Copus)

    # コサイン類似度の計算に必要な分子分母の変数
    var c = 0.0
    var m1 = 0.0
    var m2 = 0.0

    for t1c in text1Copus:
        # text2のコーパスにtext1のコーパスがあるかないかで類似度を計算することにします
        # text2のコーパスにtext1のコーパスがあれば1、なければ0を使います
        var n = 0.0
        if text2Tf.hasKey(t1c):
            n = 1.0
        
        # コサイン類似度に利用する分子分母の数値を計算
        c += (1 * n)
        m1 += 1 * 1
        m2 += n * n
    
    # コサイン類似度の計算
    if m1 == 0 or m2 == 0:
        return 0
    result = c / round(sqrt(m1) * sqrt(m2))

# 2つの文章のコサイン類似度を計算する
# ------------------------------------

# bi-gramでは同じ文章なので完全一致(1.0)
echo cosineSimilarity("今日はいい天気ですね", "今日はいい天気ですね", 2)

# bi-gramでは文章の構成ワードが一緒なので完全一致になる(1.0)
echo cosineSimilarity("今日はいい天気ですね", "いい天気ですね今日は", 2)

# bi-gramでは土地名が違うだけなのでやや類似(0.8)
echo cosineSimilarity("渋谷でお買い物", "新宿でお買い物", 2)

# bi-gramでは完全に違う文章(0.0)
echo cosineSimilarity("スイカは果物", "ピーマンは嫌い", 2)

これで文章の類似性をざっくり出すことができました。
もっと丁寧に文章を解析する手法として「形態素解析」や「構文解析」などを用いる方法があります。
これをやると精度が上がります。

今回は入門ということでn-gramを使ってみましたが、
機会があればやってみたいですね。

終わりに

ちょっと詰め込みすぎましたが

  • Nim言語は良いぞぉ!
  • n-gramを使って文章の類似度を出す

ということを行いました。
これを機会に、よかったらNim言語に手を出してみてください!

それでは今回はこのへんで。
また次回!

第2回定期勉強会「機械学習勉強会」

エキサイトの たからだ です。

先月から社内定期勉強会が弊社で始まりました(前回の様子はこちら)。第2回の今回はAWSスペシャリストの方に機械学習勉強会」をリモートで実施していただきました!

内容

機械学習初学者を対象に座学とハンズオンを実施していただきました。
座学では以下のような機械学習の基礎と AWS で利用する機械学習のサービス概要について学びました。

ハンズオンでは、 Amazon SageMaker のノートブックインスタンス環境と深層学習フレームワークの Tensorflow を利用し、MNIST とよばれる手書き数字の画像データセット機械学習のライブラリである scikit-learnで画像分類を行いました。

f:id:excite-takarada:20210727124110p:plain

k-近傍法(k-nearest neighbor)や決定木(Decision Trees)等の機械学習の手法をプログラムを書きながら試し、精度の評価も行いました。

ハンズオンの後半では、深層学習 (Deep Learning) への入り口としてニューラルネットワークと畳み込みニューラルネットワークについても実際に手を動かして試しました。

全体的な感想

個人的な勉強会の感想としては、座学で機械学習の全体像やユースケースを丁寧に説明していただき、ハンズオンではユースケースの一例として画像分類を試すという流れだったので、内容を理解しながら進めることができたと思いました。

他にも勉強会後のアンケートでは以下のような声がありました。

  • 興味はあったものの手をつけられていなかったのでハンズオンで体験できてよかった
  • ハンズオンだけだと、なんとなくわかった気になってそこで満足して終わりというのが多いので、座学からのハンズオンという流れが良かった
  • 内容がとても丁寧で、この時間集中して受ければ機械学習自体の勉強にもなり、AWSの機能がどんな感じかざっくり把握できた
  • 具体的な箇所まで教えてもらえたので、実際の仕事などで使うための足がかりとすることができた

ハンズオンができてよかったという声が多く、未知のことを学ぶときはハンズオンが大事ということを再認識させてもらいました。参加していただいた方がAWSを使って機械学習を試してみるという土台はできたかなという印象です。
また、座学やハンズオンでの質問タイムでは時間いっぱいまでスペシャリストの方に質問していて有意義な時間になったと感じました。

勉強会の内容とは直接関係ないですが、今回の勉強会の構成は1.5時間の座学と3時間のハンズオンでした。両方を1日で実施すると業務的に参加できない人が出てくると見込み、座学とハンズオンは週をまたいで実施していただきました。参加人数は座学が29人、ハンズオンが18人で座学だけ参加していただいた方もいたので、座学とハンズオンを週またぎで実施した試みも悪くはなかったかなと思いました。

最後に

機械学習初学者向けにとても分かりやすい座学とハンズオンで、機械学習は何も分からない状態から脱出することができました。
勉強会を調整していただいた日頃からお世話になっているSAの方、今回登壇していただいたスペシャリストの方に感謝申し上げます。
引き続き、社内の定期勉強会のレポートを紹介していきますので見ていていただけると幸いです。