バーチャルライバー・鷹宮リオンのツイートのネガポジ分類をやってみた
この記事 is 何?
TL;DR
- にじさんじのバーチャルライバー・鷹宮リオンさんのTweetのネガポジ分類を行った
- モデルはTransformerをPyTorch + PyTorch Lightningで実装した
- おうちでバーチャルライバーの配信を見よう!
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
ほぼ毎日ゲーム・雑談の生配信をメインとして活動しており、面白いトークや時折ポンコツさを思わせるゲーム実況でリスナーを楽しませてくれます。
唯一無二なセンスから生み出される「きちゃ!」や「やび!」、「い゛!?」などの独特な口癖はリスナーや他ライバーも思わず口ずさんでしまうインフルエンサーの一面も。
先日にはチャンネル登録者数20万人を達成しました。おめでとうございます。
何をやったか
感情分析
記事に書いた通り今回は鷹宮リオンさんのツイートデータを用いて感情分析をテーマにします。
感情分析とは一口に言うと、文章の単語や文脈を解析して内容がポジティブあるいはネガティブかを推測するような分析です。
タスク設定
今回は勉強に使っている「PyTorchによる発展ディープラーニング」(通称:PyTorch本。PyTorchの良い勉強になる良書です。)の7・8章の内容をベースとして鷹宮リオンさんのツイートがネガティブ or ポジティブどちらに属しているかを予測するタスクに設定します。
また、PyTorchの勉強として予測モデルにはGoogle翻訳のアルゴリズムでもある「Transformer」を実装したいと思います。(モデル実装ありきの記事内容ですがあしからず・・・)
Experiments
Workflow
ツイートのネガポジ分類は下図のようなワークフローで行いました。
今回用いるデータは 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およびその派生モデルが有名ですね。
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を行い潜在表現を得るというやり方です。
今回は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_end
がvalidation_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です。
大体月一くらいの頻度で開催されており社内・社外の登壇者による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内にあるこちらのデータを今回は使用させていただきました。
(僕も日々kaggleに取り組んではいるのですがいまだメダルもない雑魚contributorなので網膜コンペ頑張っていきたい・・・。)
データの内容について
今回はコーヒー生豆の生産情報(生産国, 品種, 精製法 etc.)とカッピングスコアがまとめられています。 まず、コーヒー生豆とは何ぞや?という感じですが、そもそもコーヒー豆の正体はコーヒーチェリーと呼ばれるサクランボのような赤い果実の種子です(下記画像参照)。その種を焙煎することで日頃僕たちが目にする茶色のコーヒー豆になります。
収穫されたコーヒーチェリーは精製処理を通して生豆として各消費国に輸出されていきます。精製処理というのはコーヒーチェリーから種子を取り出してコーヒー生豆として出荷できるような状態にする前処理のことを指します。
サクランボを想像して頂くとイメージが湧きやすいと思いますが、コーヒーチェリーから種子を取り出すと種子の周りにはべとべとした粘着質が付着しています。この状態で発酵槽に漬けて水洗したのが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')
このmisingnoというライブラリ、欠損値を可視化してくれるのでわかりやすいです。
ここからいろいろと前処理や今回の分析に使用しないカラムなどを除いていきました。
(Farm.Nameなどの欠損値は''UNK'''(Unknow)に置換するなど。)
(そんなに特別なことはやっていませんがまたあとでのっけます。)
分析してみよう!
データ整形も済んだところでカッピングの各評価項目の分布を眺めてみます。
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();
当初の目論見では僕はコーヒーの持つ甘味と各生産情報との関係性を探ろうとしたのですが、甘味(ほかにもクリーンさとか均一性とか)がかなり満点に偏っている状態。実際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();
メジャーな生産国が集まる中南米が多いようです。
ではカッピングの総合得点と品種の関係性を見ていきましょう。
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)
まとめ
かなり駆け足で進めてしまいました。
コーヒーの品種の説明などいろいろすっ飛ばしてしまいましたが初投稿故お許しください。(あとで追記します。
こういったコーヒーについてのデータ分析はあまり見たことがなかったのでこれから増えていけばいいなーと思っています。
今回のデータには生産者の名前だとか総重量だとか取引商社のカラムが目立つ一方で、コーヒーは農作物なのでコーヒーの甘味がどんな変数と相関があるのか?といった関係性を探るためにはもっと地質だとか乾燥日数だとかより細かい情報あるとより分析が面白くなると思いました。