MLエンジニア見習いのスクラップブック

新米機械学習エンジニアによる技術ネタや備忘録

バーチャルライバー・鷹宮リオンのツイートのネガポジ分類をやってみた

この記事 is 何?

TL;DR

Introduction

背景

どうも、あずペロ(@azupero_adolph)です。
未経験から機械学習エンジニアとして転職してから半年以上経過し、日々の業務や自己研鑽を続けていく中でもう少し具体的なアウトプットを発信していきたいと考えていました。
(ブログの更新もかなりおざなりになっていたので・・・)

アウトプットのネタとして考えたのは最近は自然言語処理(NLP)の前処理やNLPタスクで用いられるNNモデルをPyTorchで実装する勉強に時間を割いていたので、大袈裟ですが学びの成果として自分なりに何か形として残したいなと思っていました。

そこで閃きました!日々コーディングのお供として流しているバーチャルライバーのデータを使って面白いことができないか?と。

今回は「バーチャルライバー×自然言語処理」をテーマとして、にじさんじ所属のバーチャルライバー・鷹宮リオンさんのツイートのネガポジ分類をやってみました。

バーチャルライバーとは

そもそもバーチャルライバーというものをご存知でしょうか?
Wikipediaから引用すると

バーチャルYouTuber(バーチャルユーチューバー、英: virtual YouTuber)は日本発祥の、コンピュータグラフィックスのキャラクター(アバター)、またキャラクター(アバター)を用いてYouTuberとして動画投稿・配信を行う人
バーチャルYouTuber - Wikipedia

と記述されているようにキャラクターを用いてYoutubeを中心として配信活動(動画やライブ配信)をする方々を指します。

雑談からゲーム実況、歌やバラエティ番組のような企画物まで配信内容は様々です。

バーチャルYoutuberは個人 or 企業に所属する二通りの活動のやり方があります。普段私は「にじさんじ」という企業に所属しているバーチャルYoutuber(以下、バーチャルライバー)の配信をコーディングの作業用ラジオのような感覚で聞くことが多いです。

鷹宮リオンについて

本記事でフィーチャーさせて頂いたのは2018年にデビューしたにじさんじ所属のバーチャルライバー・鷹宮リオンさんです。

有数の金持ちが集う魔法学校、私立帝華高校の2年生、17歳。
政治家の娘で高飛車なツンデレタイプだが、学校では風紀委員に所属しており気さくな優等生を演じている。土日は社会見学の為メイド喫茶でバイト中。
nijisanji.ichikara.co.jp

www.youtube.com twitter.com

ほぼ毎日ゲーム・雑談の生配信をメインとして活動しており、面白いトークや時折ポンコツさを思わせるゲーム実況でリスナーを楽しませてくれます。

唯一無二なセンスから生み出される「きちゃ!」や「やび!」、「い゛!?」などの独特な口癖はリスナーや他ライバーも思わず口ずさんでしまうインフルエンサーの一面も。

先日にはチャンネル登録者数20万人を達成しました。おめでとうございます。

何をやったか

感情分析

記事に書いた通り今回は鷹宮リオンさんのツイートデータを用いて感情分析をテーマにします。
感情分析とは一口に言うと、文章の単語や文脈を解析して内容がポジティブあるいはネガティブかを推測するような分析です。

タスク設定

今回は勉強に使っている「PyTorchによる発展ディープラーニング」(通称:PyTorch本。PyTorchの良い勉強になる良書です。)の7・8章の内容をベースとして鷹宮リオンさんのツイートがネガティブ or ポジティブどちらに属しているかを予測するタスクに設定します。

また、PyTorchの勉強として予測モデルにはGoogle翻訳アルゴリズムでもある「Transformer」を実装したいと思います。(モデル実装ありきの記事内容ですがあしからず・・・)

Experiments

Workflow

ツイートのネガポジ分類は下図のようなワークフローで行いました。

f:id:azupero_adolph:20200429004523p:plain
本実験のワークフロー
今回用いるデータは Twitter API にて直近のツイート約2800件を取得しました。

コーパスとするにはかなり小規模ですがAPI的に直近3000件程度がリミットとしてあるみたいです。

取得したツイートデータはまずGoogle Spreadsheetで0:ネガティブ・1:ボジティプのアノテーションを手動でこなし、その後はテキストデータの前処理・トークナイズ・モデル実装を行いました。学習・推論はGoogle Colaboratoryにて実施しました。

テキスト前処理・コーパス作成

前処理

テキスト前処理は一般的な内容とTwitter特有のハッシュタグ、絵文字の除去を行っています。
Twitterのテキストは絵文字やハッシュタグなどがあり、テキスト前処理の重要さを思い知りました・・・。(恐らくこの処理でも十分とは言えないはず)

# テキスト前処理
def preprocessing_text(text):
    # 英語の小文字化(表記揺れの抑制)
    text = text.lower()
    # URLの除去(neologdnの後にやるとうまくいかないかも(URL直後に文章が続くとそれも除去される)))
    text = re.sub(r'https?://[\w/:%#\$&\?\(\)~\.=\+\-…]+', '', text)
    # tweetの前処理
    text = re.sub(r"@([A-Za-z0-9_]+) ", '', text) # リプライ
    text = re.sub(r'#(\w+)', '', text) # ハッシュタグ
    # neologdnを用いて文字表現の正規化(全角・半角の統一と重ね表現の除去)
    text = neologdn.normalize(text)
    # 数字を全て0に置換(解析タスク上、数字を重要視しない場合は語彙数増加を抑制するために任意の数字に統一したり除去することもある)
    text = re.sub(r'[0-90-9]+', '0', text)
    # 半角記号の除去
    text = re.sub(r'[!-/:-@【】[-`{-~]', "", text)
    # 改行
    text = re.sub('\n', '', text)
    # 絵文字
    text = ''.join(['' if c in emoji.UNICODE_EMOJI else c for c in text])
    # 中黒や三点リーダ
    text = re.sub(r'[・…]', '', text)

    return text
トークナイズ

トークナイズはMeCab + NEologdで行いました。また、単語埋め込みベクトルとしてこちらのfastTextの日本語学習済みモデルを利用させていただきました。ありがとうございます。

# MeCab + NEologdによるtokenizer
def tokenizer_mecab(text):
    tagger = MeCab.Tagger('-Owakati -d /usr/lib/x86_64-linux-gnu/mecab/dic/mecab-ipadic-neologd') # -Owakatiで分かち書きのみ出力
    text = tagger.parse(text)
    text = text.strip().split()

    return text
コーパス作成

先述したようにラベリングはNegative -> 0, Positive -> 1として、Google spreadsheet上でアノテーションを行いました。
ラベリングにおける判断基準は私の主観でつけさせていただきました。

ざっとですが一つ一つ内容を見てラベリングしていくのはいざやってみると結構大変な作業でした。

なお事前に前処理+トークナイズでNULL(トークナイズ後に単語が何も残らない)になるようなツイートはドロップしています。

データセット作成

PyTorchにおけるテキストデータのデータセットtorchtext.data.Datasetを使用します。

def get_dataset(max_length=256, split_ratio=[0.92, 0.04, 0.04]):
    PATH = '/content/drive/My Drive/Colab Notebooks/NLP/RionTweetClassifier/data/rion_corpus.csv'
    # Field
    TEXT = torchtext.data.Field(sequential=True, 
                                tokenize=tokenizer_with_preprocessing, 
                                use_vocab=True, 
                                lower=True, 
                                include_lengths=True, 
                                batch_first=True, 
                                fix_length=max_length, 
                                init_token="<cls>", 
                                eos_token="<eos>"
                                )

    LABEL = torchtext.data.Field(sequential=False, use_vocab=False, dtype=torch.float32)
    # Dataset
    ds = torchtext.data.TabularDataset(path=PATH, 
                                       format='csv', 
                                       skip_header=True, 
                                       fields=[('Text', TEXT), ('Label', LABEL)]
                                       )
    
    train_ds, test_ds, val_ds = ds.split(split_ratio=split_ratio)
    # embedding vector
    FASTTEXT = '日本語学習済みfastTextモデルのパス'
    fastText_vectors = Vectors(name=FASTTEXT)
    # build vocab
    TEXT.build_vocab(train_ds, vectors=fastText_vectors, min_freq=1)

    return train_ds, test_ds, val_ds, TEXT

テキストの前処理を管理するtorchtext.data.Fieldで先ほどのテキスト前処理+トークナイズのパイプライン関数をtokenizer_with_preprocessingで指定しています。
LABELのデータ型をtorch.floatにしているのは後述の学習・推論コード部分で損失関数を計算するためにモデルの出力とデータ型を一致させるためです。

torchtext.data.TabularDatasetでデータセットを作成し.split()メソッドでsplit_ratioによりtrain set, valid set, test setに3分割します。
今回の比率はできるだけtrain setを多くしたいと思いtrain:test:valid = 0.92:0.04:0.04とさせていただきました。

Transformerについて

Transformerとは

2017年に発表された論文「Attention Is All You Need」で提案されたモデルです。Google翻訳アルゴリズムとして、自然言語処理におけるニューラルネットワークモデルに衝撃を与えました。現在はTransformerをベースとしたBERTおよびその派生モデルが有名ですね。

f:id:azupero_adolph:20200427002645p:plain
Vaswani, A. et al.(2017) Attention Is All You Need

Transformerの理論面は以下の記事で非常に詳しく解説されていますので興味のある方は是非参照してみて下さい。

deeplearning.hatenablog.com qiita.com

kaggleのNLPコンペなどではBERTがベースラインと用いられているなど、どちらかというと今はBERTの時代のような流れですがまずはそのベースとなっているTransformerの理解を実装と通じて深めたいと思います。なおPyTorch本同様、モデルはEncoder部分 + Classificationモジュールでの実装となります。

Multi-Head Attentionの実装

Transformerの実装についてはPyTorch本や参照元を大いに参考にさせていただきました。

モデルの実装部分はほぼこちらに則っていますがPyTorch本ではMulti-Head AttentionではなくシングルヘッドのSelf-Attentionとして実装されていますので本家の通りMulti-Headに差し替えました。

Multi-Head AttentionはSelf-Attentionを複数のヘッド、即ち入力のembeddingを分割し個々にSelf-Attentionを行い潜在表現を得るというやり方です。

f:id:azupero_adolph:20200427003852p:plain
Vaswani, A. et al.(2017) Attention Is All You Need
f:id:azupero_adolph:20200427003858p:plain
Vaswani, A. et al.(2017) Attention Is All You Need

今回はMultiHeadAttentionクラスとして実装しています。

class MultiHeadAttention(nn.Module):
    def __init__(self, heads, d_model, dropout=0.1):
        super().__init__()

        self.d_model = d_model
        self.d_k = d_model // heads
        self.h = heads

        self.q_linear = nn.Linear(d_model, d_model)
        self.v_linear = nn.Linear(d_model, d_model)
        self.k_linear = nn.Linear(d_model, d_model)
        self.dropout = nn.Dropout(dropout)
        self.out = nn.Linear(d_model, d_model)

    def forward(self, q, k, v, mask=None):
        bs = q.size(0)
        # perform linear operation and split into h heads
        k = self.k_linear(k).view(bs, -1, self.h, self.d_k)
        q = self.q_linear(q).view(bs, -1, self.h, self.d_k)
        v = self.v_linear(v).view(bs, -1, self.h, self.d_k)

        # transpose to get dimensions bs * h * sl * d_k
        k = k.transpose(1, 2)
        q = q.transpose(1, 2)
        v = v.transpose(1, 2)

        # calculate Scaled Dot-Product Attention
        output, normalized_attention_weight = scaled_dot_product_attention(q, k, v, self.d_k, mask, self.dropout)

        # concatenate heads and put through final linear layer
        # concat = scores.transpose(1, 2).contiguous().view(bs, -1, self.d_model)
        concat = output.transpose(1, 2).reshape(bs, -1, self.d_model)
        output = self.out(concat)

        return output, normalized_attention_weight

学習・推論 with PyTorch Lightning

学習・推論コードはPyTorch Lightningを用いてGoogle Colaboratory上で実装しています。
ピュアなPyTorchの書き方ではforループを繰り返していく必要がありましたがこちらのフレームワークを用いることで簡略化・分かりやすく記述することが可能です。 Google Colaboratory上でも楽に学習曲線をTensorBoardでモニタリングできるのが嬉しいですね。

loss functionはBCEWithLogitsLoss、optimizerはAdamです。

以前にもPyTorch Lightningは触っていたんですが最新版はデコレーターが非推奨だったり、validation_endvalidation_epoch_endになっていたりと仕様が変わっていますね。 あと地味に苦しんだのはtorchtext.data.Iteratorで返すバッチデータは自動的にGPUに転送されないので明示的に.to(device)GPUにデータを送る必要があるという。以前は画像データでやっていたので知らなかったです。

class MyLightningModule(pl.LightningModule):
    def __init__(self, model, train_ds, val_ds, test_ds, batch_size):
        super().__init__()
        # dataset path
        self.train_ds = train_ds
        self.val_ds = val_ds
        self.test_ds = test_ds
        self.bs = batch_size
        self.model = model

    def forward(self, x):
        # モデルの順伝搬処理
        input_mask = src_mask(x)
        x = self.model(x, input_mask)

        return x

    def training_step(self, batch, batch_idx):
        # train setのmini-batchにおける処理
        # REQUIRED
        x = batch.Text[0].to(device)
        y = batch.Label.unsqueeze(1).to(device)
        y_hat = self.forward(x)

        criterion = nn.BCEWithLogitsLoss(reduction='sum')
        loss = criterion(y_hat, y)
        tensorboard_logs = {'train_loss': loss.item()}

        return {'loss': loss, 'log': tensorboard_logs}

    def validation_step(self, batch, batch_idx):
        # valid setのmini-batchにおける処理
        x = batch.Text[0].to(device)
        y = batch.Label.unsqueeze(1).to(device)
        out = self.forward(x)
        
        bs = len(batch.Label)
        pred = torch.where(out >= 0.5, torch.ones(bs, 1).to(device), torch.zeros(bs, 1).to(device))

        criterion = nn.BCEWithLogitsLoss(reduction='sum')
        loss = criterion(out, y)

        return {'val_loss': loss, 'label': y, 'pred': pred}

    def test_step(self, batch, batch_idx):
        # test setのmini-batchにおける処理
        x = batch.Text[0].to(device)
        y = batch.Label.unsqueeze(1).to(device)
        out = self.forward(x)
        
        bs = len(batch.Label)
        pred = torch.where(out >= 0.5, torch.ones(bs, 1).to(device), torch.zeros(bs, 1).to(device))

        criterion = nn.BCEWithLogitsLoss(reduction='sum')
        loss = criterion(out, y)

        return {'test_loss': loss, 'label': y, 'pred': pred}

    def validation_epoch_end(self, outputs):
        # valid setのmini-batch処理終了時の処理
        avg_loss = torch.stack([x['val_loss'] for x in outputs]).mean()
        # accuracy
        acc = torch.mean(torch.cat([(output['label'] == output['pred']) * 1.0 for output in outputs]))

        tensorboard_logs = {'val_loss': avg_loss}
        tqdm_dict = {'avg_val_loss': avg_loss, 'val_acc': acc}

        return {'progress_bar': tqdm_dict, 'avg_val_loss': avg_loss, 'log': tensorboard_logs}

    def test_epoch_end(self, outputs):
        # test setのmini-batch処理終了時の処理
        avg_loss = torch.stack([x['test_loss'] for x in outputs]).mean()
        # accuracy
        acc = torch.mean(torch.cat([(output['label'] == output['pred']) * 1.0 for output in outputs]))

        tensorboard_logs = {'test_loss': avg_loss}
        tqdm_dict = {'avg_test_loss': avg_loss, 'test_acc': acc}

        return {'progress_bar': tqdm_dict, 'avg_val_loss': avg_loss, 'log': tensorboard_logs}

    def configure_optimizers(self):
        # 最適化手法・学習率スケジュールの設定
        # REQUIRED
        optimizer = optim.Adam(self.parameters(), lr=1e-5)
        scheduler = optim.lr_scheduler.CosineAnnealingLR(optimizer, T_max=10)
        
        return [optimizer], [scheduler]

    # @pl.data_loader # 最新版はデコレーターは非推奨
    def train_dataloader(self):
        # REQUIRED
        train_loader = torchtext.data.Iterator(train_ds, batch_size=self.bs, train=True)
        
        return train_loader

    # @pl.data_loader
    def val_dataloader(self):
        # OPTIONAL
        val_loader = torchtext.data.Iterator(val_ds, batch_size=self.bs, train=False, sort=False)
        
        return val_loader

    def test_dataloader(self):
        test_loader = torchtext.data.Iterator(val_ds, batch_size=self.bs, train=False, sort=False)

        return test_loader

# early stopping callback
early_stop_callback = EarlyStopping(min_delta=0.00,
                                    patience=5,
                                    verbose=True,
                                    monitor='val_loss',
                                    mode='min')

Results

学習・推論結果

Test setにおけるAccuracyは95%でした。ただやはりデータセット自体が小規模・imbalancedですのでRecall, Precisionも求める必要がありそうです。

(やはりモデル自体がヘビーすぎる感が否めない)

Attentionの可視化

PyTorch本のようにいくつかのツイートを例にしてAttentionの可視化を試みたのですが、なんとattention weightをキャッシュしようとするとメモリが足らなくなる事態に。
後ほどHeadsを少なくしたモデルでの可視化を試みたいと思います。

Conclusion

今回はPyTorchとTransformerの勉強を兼ねてバーチャルライバーのネガポジ分類を行いました。ネガポジ分類は単語ごとのネガポジのスコアを足し合わせる方法もありそうですし、単語のtf-idfを特徴量としてGBDTで推定するなどの方法もあります。
まだまだNLP初心者の身ですが、前処理からモデル構築、PyTorch Lightningでの学習・推論などPyTorchでNLPをやるという基本的な流れを学べましたし、 BERTのベースとなるTransformerについても理解を深めることができました。

最近は自宅にいる時間が増えているという方もいらっしゃると思います。
是非この機会にバーチャルライバーの配信を覗いてみるのはいかがでしょうか?

白金鉱業 Vol.10(一周年記念会)に行ってきた

お世話になっております。あずペロ(@azupero_adolph)です。 今回はデータサイエンティスト / 機械学習エンジニアの勉強会兼交流会として人気イベントである白金鉱業 Vol.10へお邪魔したので簡単にレポートをまとめてみました。

これは何?

  • DS / MLの勉強会兼交流会の白金鉱業に行ってきた(初参加)
  • 各LTや交流会の感想
  • 楽しかった(重要)

白金鉱業って?

データ分析を手掛ける株式会社BrainPadさんに所属しているデータサイエンティストが有志で企画しているデータ分析に従事するエンジニアたちのMeetupです。

brainpad-meetup.connpass.com

大体月一くらいの頻度で開催されており社内・社外の登壇者によるLTが複数あり、技術寄り、ビジネス寄りなどテーマに沿った内容のプレゼンが展開されています。またLTの後は参加者たちとの交流会が恒例となっているようです。

なぜ参加しようと思ったのか?

一つは機械学習やデータ分析の技術的知見を深めようと思い応募してみました。自分自身も現在はデータ分析業務に携わらせてもらっていますが、自職場ではなかなか専門的な内容については議論できる機会がありません。なのでつよつよなデータサイエンティスト集団を抱えるBrainPadさんが主催しているこの勉強会は自分の引き出しを増やす、気付きを得るまたとない機会だと思っていたからです。

二つ目はエンジニア同士の交流会です。自分の業界はIT系ではないために周りにデータサイエンティストや機械学習エンジニアと呼ばれるような人とはなかなかつながりが持てず、実際に業務に携わっている方々との交流はエンジニアの実際を知る上で大事だと感じていたからです。また現在機械学習エンジニアとして転職活動を行っているので色々とお話を伺ってみたかったというのもあります。

以下LTの内容をメモを元に感想をだらだらと書いていきます。

1. 機械学習で稼ぐための会計の話

登壇者:中山ところてん氏(株式会社NextInt)
(LT資料については後日公開予定とのことです。)

よくTwitterでお見掛けする方です。LTテーマは"機械学習とビジネス"。機械学習エンジニアやデータサイエンティストもビジネスマンの一員だから相手に伝えるときは相手側に伝わる言葉で自分のビジネスを語るべき。そのための共通言語として会計の知識は必要不可欠だよねという趣旨のお話でした。

会計の知識(固定費・変動費・利益・減価償却費)を元に機械学習案件をテーマとして機械学習システム・設備を導入するタイミングや、導入による利益変動の考え方といった内容を分かりやすく解説して頂きました。自分も学部生時代に履修した会計学の知識を思い出しながらこれまでキチンと考えてこなかった機械学習ビジネスの税制面との相性について学ぶことができました。

2. 音声データでつくるライフフィットメディア

登壇者:緒方憲太郎氏(株式会社Voicy)

2本目は音声メディアであるVoicyの代表である緒方さんによるLT。"音声データって面白い!"というメッセージを前面に出したプレゼンに終始惹きつけられました。今の時代はモニターから出力される映像や文字からスマホへと人間への情報伝達のファーストリーチが変化していて、今は更にスマートスピーカーの登場によって今後は音声がファーストリーチになる。そこで音声インフラ産業のプラットフォーム ・VUX文化作りをやろうという思いで設立に至ったという背景が面白かったです。

確かに最近はポッドキャスト市場も賑わいも見せ、スマートスピーカーもどんどん市場に登場しています。LT中にも仰られていましたが、印象的なワードは"音声データは感情(気持ち)が込められている"でした。音声データから得られるのはテキスト的な情報だけでなく、発話者の感情が包含されているということです。これを感情分析として捉え蓄積されたデータを可視化することでビジネス応用に繋がります。音声広告の不快率が低いというお話も印象的でした。音声広告は飛ばしたくならないというのは面白い話だなと。

3. データサイエンス系の勉強会を1年間毎月開催したら何が起こるか? ~白金鉱業1周年のまとめ~

登壇者:吉田勇太氏(株式会社BrainPad)
(こちらもLT資料については後日公開予定とのことです。)

最後は白金鉱業主催者でもあるBrainPadさんのリードデータサイエンティストである吉田さん。勉強会を主催する上での日程調整や準備といった設計方針・計画についてどのように行ってきたかをこれまでのデータを交え、本イベントを1年間続けてきたうえで得られた知見をご紹介いただきました。今後こういったエンジニア同士の勉強会・交流会を開いてみたい!という人にとっては(自分含め)非常に参考になる点が多かったです。

こういったイベントに参加するメリットとしてはモチベーション向上のウェイトが大きいと自分は感じています。社外の登壇者による技術的・ビジネス的なLTは非常に勉強になりますしLTという短い時間で集中して新しい知識が取り込めるのはとても良い刺激になると考えています。また、刺激をもらうという点においてはその後の懇親会でのエンジニア同士のコミュニケーションも大事です。社外のエンジニアと技術的なことをざっくばらんに話せる交流の場というのは自分の見解を広げるうえでまたとない機会だなと思っています。それは別に技術的なことでなくてもDS・ML屋の辛みといった愚痴でも構わないと思っています。

参加者の方々は「技術的なことを学び、持ち帰りたい」という人や「エンジニア同士の交流をお酒を交えて楽しみたい」だとか各々目的を持って参加されている訳ですから、勉強会兼交流会というイベントはこのような目的をもつ人々にとってそれを満足させるようなまたとない機会ですし、これに参加することは一種のデータサンプリングなのではないかなとも思っています。

4. 交流会

LT3本が終了した後は用意して頂いたピザや唐揚げ(7000円分!)とお酒で楽しく懇親会。同じテーブルを囲んでいたデータサイエンティストの方々と色々と情報交換を行うことが出来てとても楽しかったです。

5. マスクド・アナライズ氏による飛び入り?LT

交流会も終わりに差し迫ったころにアナウンス。自分もTwitter上でよくお見掛けするマスクド・アナライズさんによるLTが。AIブームとプロレスブームとの類似性を紹介しながら今後のAIブームを一過性にしないためにどうすればいいか?という提案でした。 AI活用の実現にはそれを支える制度や環境作りが不可欠という納得のプレゼン。持論ですがAI活用で業務改善!系が残念ながら頓挫してしまうのはお互いの"AIに対する理解度"の乖離が原因だと考えています。なのでAI人材をきちんと育てられるような土壌づくりや、エンジニア以外も機械学習に対して理解を深めていくことが求められるかもしれません。

6. ボストンコンサルティングさんによるLT

飛び入りのLTはもう一本ありました。AI人材育成を目的として、社会課題をテーマとして機械学習の実装スキルやビジネス課題能力の育成プログラムの紹介がありました。単にコーディングをするだけでなくビジネス課題の要件定義から機械学習のタスクに落とし込むまでのフローを学べる感じでとても面白そうだなと思っています。

まとめ

平日なので移動がきつかったんですが参加して本当に良かったなと思えるMeetupでした。今回のLTは機械学習ビジネスについての知見を得ることが出来ましたし、交流会ではDSの方々とのお話もすごく充実しました。またなにより自分自身もこの業界への転職に向けてモチベーションがまだ一段と上がり、一層自己研鑽に励みたいと思います。
設営していただいたスタッフの皆様、登壇者の皆様、参加者の皆様ありがとうございました!

kaggle Datasetsのコーヒー生豆のデータ分析やってみた

はじめまして。あずペロ(@azupero_adolph)と申します。 このブログは機械学習統計学などの技術的アウトプットを残していきたいなーと思って開設してみました。
初投稿は趣味であるコーヒーに関するデータで(サクッと)分析やってみた系をアップします。 美味しいコーヒーなどを準備して時間つぶし程度に見ていただければ幸いです。
内容に関しては随時アップデートしていきます。

これはなに?
  • kaggle Datasets内のコーヒー生豆に関するデータを使って分析・可視化
  • コーヒー生豆のカッピング評価と生産国や品種の関係性を探る
使用したデータについて

コーヒーに関するデータで色々やりたいなーと思って探していくうちにkaggleのDatasets内にあるこちらのデータを今回は使用させていただきました。

www.kaggle.com

(僕も日々kaggleに取り組んではいるのですがいまだメダルもない雑魚contributorなので網膜コンペ頑張っていきたい・・・。)

データの内容について

今回はコーヒー生豆の生産情報(生産国, 品種, 精製法 etc.)とカッピングスコアがまとめられています。 まず、コーヒー生豆とは何ぞや?という感じですが、そもそもコーヒー豆の正体はコーヒーチェリーと呼ばれるサクランボのような赤い果実の種子です(下記画像参照)。その種を焙煎することで日頃僕たちが目にする茶色のコーヒー豆になります。

http://lightupcoffee.com/wp-content/uploads/2016/05/pngbase64f5fb45529900dfeb-1024x680.png lightupcoffee.com

収穫されたコーヒーチェリーは精製処理を通して生豆として各消費国に輸出されていきます。精製処理というのはコーヒーチェリーから種子を取り出してコーヒー生豆として出荷できるような状態にする前処理のことを指します。 サクランボを想像して頂くとイメージが湧きやすいと思いますが、コーヒーチェリーから種子を取り出すと種子の周りにはべとべとした粘着質が付着しています。この状態で発酵槽に漬けて水洗したのがWashedと呼ばれる精製法でコーヒー生豆のもつ成分がクリアに現れます。
Washedとは別にコーヒーチェリーのまま乾燥させて脱穀させるというNaturalという精製処理があり、Washedに比べワイニーな芳香が印象的になります。 最後にHoneyも今回のデータで登場しますが、これはNaturalに似ているんですが果実の状態のままではなくて果肉を取り除いた状態で乾燥させる方法になり、一般的にはWashedとNaturalの中間のフレーバーに位置するとよく表現されます。

では、カッピングスコアとは何でしょうか?

簡単に言えばコーヒーの風味を総合的にスコアリングして評価することを指します。
評価項目としては

  • アロマ
  • フレーバー
  • アフターテイスト
  • 酸味
  • ボディ
  • バランス
  • 均一性
  • クリーン性
  • 総合
  • 欠点

でトータル100点満点でスコアリングしてきます。この得点が80点を超すといわゆるスペシャルティコーヒーと呼ばれる豆になります。

データ前処理

さて、前置きが長くなってしまいましたが早速データの中身を見ていきたいと思います。

まずはライブラリとデータのインプットから。

# import library
import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
import seaborn as sns
import missingno as msno

%matplotlib inline

# input data
df = pd.read_csv('Coffee-modified.csv')

データの形状をまずは見てみましょう。

# DataFrameの形状チェック
print('Count of row : {} & Count of column : {}'.format(df.shape[0], df.shape[1]))
Count of row : 1319 & Count of column : 44
  • レコード数としては比較的ミニマムなデータ
  • もっといいスペシャルティコーヒーのデータがあればぜひ教えてください~

次に欠損値を含めたDataFrameの情報をチェックします。

# DataFrameの形状に加えてデータ型などを確認
df.info()
<class 'pandas.core.frame.DataFrame'>
RangeIndex: 1319 entries, 0 to 1318
Data columns (total 44 columns):
ID                       1312 non-null object
Species                  1319 non-null object
Owner                    1310 non-null object
Country.of.Origin        1316 non-null object
Farm.Name                961 non-null object
Lot.Number               276 non-null object
Mill                     1007 non-null object
ICO.Number               1169 non-null object
Company                  1105 non-null object
Altitude                 1092 non-null object
Region                   1257 non-null object
Producer                 1084 non-null object
Number.of.Bags           1314 non-null object
Bag.Weight               1314 non-null object
In.Country.Partner       1314 non-null object
Harvest.Year             1266 non-null object
Grading.Date             1313 non-null object
Owner.1                  1305 non-null object
Variety                  1110 non-null object
Processing.Method        1159 non-null object
Aroma                    1311 non-null object
Flavor                   1309 non-null object
Aftertaste               1309 non-null object
Acidity                  1309 non-null object
Body                     1309 non-null float64
Balance                  1309 non-null float64
Uniformity               1309 non-null float64
Clean.Cup                1307 non-null float64
Sweetness                1308 non-null float64
Cupper.Points            1308 non-null object
Total.Cup.Points         1308 non-null object
Moisture                 1308 non-null object
Category.One.Defects     1308 non-null object
Quakers                  1307 non-null object
Color                    1092 non-null object
Category.Two.Defects     1307 non-null float64
Expiration               1307 non-null object
Certification.Body       1307 non-null object
Certification.Address    1307 non-null object
Certification.Contact    1307 non-null object
unit_of_measurement      1307 non-null object
altitude_low_meters      1081 non-null float64
altitude_high_meters     1081 non-null float64
altitude_mean_meters     1081 non-null float64
dtypes: float64(9), object(35)
memory usage: 453.5+ KB
  • 多数の欠損値が存在する特徴量がある
  • 欠損値が本当に欠損値なのかを見てみる必要がある(情報を持たないor欠損値自体が情報を持つ)
# ざっと欠損値の確認
ax = msno.matrix(df)
ax.tick_params(color='w', labelcolor='w')

f:id:azupero_adolph:20190729232842p:plain このmisingnoというライブラリ、欠損値を可視化してくれるのでわかりやすいです。

ここからいろいろと前処理や今回の分析に使用しないカラムなどを除いていきました。
(Farm.Nameなどの欠損値は''UNK'''(Unknow)に置換するなど。)
(そんなに特別なことはやっていませんがまたあとでのっけます。) f:id:azupero_adolph:20190729233446p:plain

分析してみよう!

データ整形も済んだところでカッピングの各評価項目の分布を眺めてみます。

cupping_list = ['Aroma', 'Flavor', 'Aftertaste', 'Acidity', 'Body', 'Balance', 'Uniformity', 
                'Clean.Cup', 'Sweetness', 'Cupper.Points', 'Total.Cup.Points']

with plt.style.context('ggplot'):
    df[cupping_list].hist(bins=25, figsize=(20, 15), ec='k')
    plt.show();

f:id:azupero_adolph:20190729233908p:plain

当初の目論見では僕はコーヒーの持つ甘味と各生産情報との関係性を探ろうとしたのですが、甘味(ほかにもクリーンさとか均一性とか)がかなり満点に偏っている状態。実際25%タイルで満点になっていることもあり、扱いが難しいデータでした。

気を取り直して今回のデータの生産国について。どの国の情報が多いのでしょうか。

with plt.style.context('bmh'):
    fig = plt.figure(figsize=(15,10))
    ax = fig.add_subplot(1,1,1)
    ax.set_xlabel(_, color='w')
    ax.set_ylabel(_, color='w')
    ax.tick_params(color='w', labelcolor='w')
    sns.countplot(data=df, y=df['Country.of.Origin'], order=list(df['Country.of.Origin'].value_counts().index))
    plt.show();

f:id:azupero_adolph:20190729234907p:plain

メジャーな生産国が集まる中南米が多いようです。

ではカッピングの総合得点と品種の関係性を見ていきましょう。

with plt.style.context('bmh'):
    fig = plt.figure(figsize=(30,10))
    ax = fig.add_subplot(1,1,1)
    ax.set_xlabel(_, color='w')
    ax.set_ylabel(_, color='w')
    ax.tick_params(color='w', labelcolor='w')
    sns.barplot(y='Variety', x='Total.Cup.Points', data=df)
    ax.set_xlim(80,88)

f:id:azupero_adolph:20190729235146p:plain

  • 平均値で見るとSumatora Lintong(飲んだことない)が高いですね。
  • 全体のレンジで見るとコーヒーの起源でもあるエチオピアの原生種や、ケニアでよく栽培されている品種のSL34も目立ちます。
まとめ

かなり駆け足で進めてしまいました。
コーヒーの品種の説明などいろいろすっ飛ばしてしまいましたが初投稿故お許しください。(あとで追記します。
こういったコーヒーについてのデータ分析はあまり見たことがなかったのでこれから増えていけばいいなーと思っています。
今回のデータには生産者の名前だとか総重量だとか取引商社のカラムが目立つ一方で、コーヒーは農作物なのでコーヒーの甘味がどんな変数と相関があるのか?といった関係性を探るためにはもっと地質だとか乾燥日数だとかより細かい情報あるとより分析が面白くなると思いました。