u++の備忘録

2023 年をザッと振り返る

年末恒例の振り返り記事です。 2021 年 4 月の部署異動を契機に、今年も研究開発部署にて案件を自分自身で創出・推進していくことに挑戦した一年でした。 特に「ChatGPT」に代表される大規模言語モデルが社会一般に認知されたことで、ありがたいことにここ数年の自分の取り組みへの注目度も上がったと感じました。

本記事では、対外公表している事例の一覧をまとめました。 社内プロジェクトで公表できている部分は多くないですが、今年は 2 件の社内表彰を頂けました。 来年も事業貢献と研究活動の両面を追い求めていければと思っています。

査読付き国際学会・ワークショップ

データマイニングの「CIKM」、計算社会科学の「IC2S2」など、主要な国際会議に採択されました。7 月 17〜20 日にデンマークコペンハーゲンで開催された「IC2S2」には現地参加でき、有意義な時間を過ごせました。

ACL 2023 のワークショップに採択された「Training Data Extraction From Pre-trained Language Models: A Survey」は単著のサーベイ論文です。このサーベイの起点とした論文の著者の新着論文で引用されたのは、研究コミュニティへの貢献を感じられる非常に感慨深い体験になりました。

  • Kaito Majima†, and Shotaro Ishihara† (2023). Generating News-Centric Crossword Puzzles As A Constraint Satisfaction and Optimization Problem. Proceedings of the 32nd ACM International Conference on Information and Knowledge Management (CIKM 2023). Association for Computing Machinery. (†equal contribution) [arXiv] [paper]
  • Shotaro Ishihara, Hiromu Takahashi, and Hono Shirai (2023). Quantifying Diachronic Language Change via Word Embeddings: Analysis of Social Events using 11 Years News Articles in Japanese and English. 9th International Conference on Computational Social Science (IC2S2 2023). [abstract] [poster]
  • Shotaro Ishihara (2023). Training Data Extraction From Pre-trained Language Models: A Survey. Proceedings of Third Workshop on Trustworthy Natural Language Processing. [arXiv] [paper] [poster]

国内学会・研究会発表

  • 石原祥太郎, 高橋寛武 (2023). ニュース記事の逆ピラミッド構造は読みやすさ評価に使えるか. NLP若手の会 (YANS) 第18回シンポジウム.
  • 村田栄樹, 石原祥太郎 (2023). ドメイン別に訓練した要約モデルにおけるHallucinationの内在・外在要因分析. NLP若手の会 (YANS) 第18回シンポジウム.
  • 増田太郎, 櫻井亮佑, 桐井智弘, 渡邊英介, 石原祥太郎 (2023). 企業・業界動向抽出のための経済情報ラベルの定義とタグ付きコーパスの構築. NLP若手の会 (YANS) 第18回シンポジウム.
  • 石原祥太郎, 中間康文 (2023). マルチモーダル機械学習によるニュース記事の閲覧時間予測. 2023年度人工知能学会全国大会(第37回)論文集.
  • 石原祥太郎 (2023). 事前学習済み言語モデルからの訓練データ抽出:新聞記事の特性を用いた評価セットの構築と分析. 言語処理学会第29回年次大会発表論文集. [paper]
  • 大村和正 (京大), 白井穂乃, 石原祥太郎, 澤紀彦 (2023). 極性と重要度を考慮した決算短信からの業績要因文の抽出. 言語処理学会第29回年次大会発表論文集. [paper]
  • 石原祥太郎, 高橋寛武, 白井穂乃 (2023). 単語分散表現による言語の通時変化の定量化:11年分の日英ニュース記事を用いた社会的事象の分析. 第2回計算社会科学会大会(CSSJ2023). (大会優秀賞 [website])

書籍

講談社から共著で『Kaggleに挑む深層学習プログラミングの極意』を出版しました。 画像・自然言語処理機械学習コンテストを題材として、深層学習ライブラリ「PyTorch」での実装を交えながら、著者らの経験に基づく知見をまとめました。

upura.hatenablog.com

ニューズレター

ニューズレター「Weekly Kaggle News」が本日 4 周年を迎えました。 日本語で、Kaggleをはじめとするデータ分析コンペティションに関する話題を取り扱っています。 週次で毎週金曜日に更新しており、最新は第 211 号、購読者数は約 2700 人になりました。 今年からプラットフォームを Substack に変更しました。

upura.hatenablog.com

受賞

2 月に第 2 回計算社会科学会大会で発表した「単語分散表現による言語の通時変化の定量化:11年分の日英ニュース記事を用いた社会的事象の分析」で、優秀賞を頂きました。12 月には Google Cloud の Champion Innovator (Cloud AI/ML 領域) にご選出いただきました。

イベント登壇

インタビュー・メディア掲載

【Weekly Kaggle News 4 周年】記事閲覧数ランキング 2023

Kaggle Advent Calendar 2023」の 20 日目の記事です。

ニューズレター「Weekly Kaggle News」が本日 4 周年を迎えました。日本語で、Kaggleをはじめとするデータ分析コンペティションに関する話題を取り扱っています。週次で毎週金曜日に更新しており、最新は第 209 号、購読者数は 2680 人になっています。今年からプラットフォームを Substack に変更しました*1。メール配信の性質上あまり実感が湧きづらいのですが、今年だけで約 350 人も購読者数が増えたようで驚きです。

それでは、今年発行の Weekly Kaggle News 経由で閲覧された URL のランキング結果を紹介します。単純なクリック回数なので、購読者数が増えている直近の回が有利な条件になっています。なお過去分もランキングを公開しています。

見落としていた記事があれば、ぜひご覧ください。

1 位: 124 クリック(#192)

1 位は、今年 6 月に終了した Kaggle「Vesuvius Challenge - Ink Detection」で Grandmaster に昇格した tk さん による振り返り記事。コンペの取り組み方と、参加した各コンペの概要・解法が綴られています。

tnkcoder.hatenablog.com

2 位: 112 クリック(#206)

2 位は、年末に出版される書籍『事例で学ぶ特徴量エンジニアリング』(オライリー・ジャパン)が入りました。特徴量エンジニアリングの基本概念や事例が掲載されている書籍だそうです。

https://www.amazon.co.jp/dp/4814400543

3 位: 105 クリック(#165)

テーブルデータを高速に処理する Polars は、今年に急速に認知度が高まったライブラリと言えるかもしれません。定番の Pandas との比較記事が 3 位になりました。

zakopilo.hatenablog.jp

4 位: 103 クリック(#163)

昨年末から今年にかけて開催された Kaggle「OTTO – Multi-Objective Recommender System」では、不正行為が大きな話題となりました。この話題に関する Kaggle 上の投稿が 4 位にランクインです。

www.kaggle.com

5 位: 101 クリック(#200)

5 位は、Kaggle の画像コンペを題材にした入門資料の後編。前編の資料も公開されています。今年も数多くの画像コンペが開催されました。

6 位: 97 クリック(#166)

ChatGPT に代表される大規模言語モデルが大きな話題となった今年、その土台となる Transformer 機構の解説記事が 6 位に入りました。構造や応用を幅広く扱って解説しています。

zenn.dev

7 位: 97 クリック(#184)

7 位は、テーブルデータに対してニューラルネットワークを適用する手法の検証記事。今なお根強い人気のある勾配ブースティング決定木と比較しています。

note.com

8 位: 92 クリック(#181)

深層学習モデルの高速化手法のまとめ資料が 8 位になりました。畳み込みニューラルネットワー(CNN)と Vision Transformer を中心に掘り下げています。

9 位: 91 クリック(#182)

9 位は、今年 6 月に出版された『LightGBM予測モデル実装ハンドブック』(秀和システム)。勾配ブースティング決定木の LightGBM の理論と実践のための書籍です。

https://www.amazon.co.jp/dp/479806761X

10 位: 88 クリック(#189)

自作の機械学習パイプラインを紹介している記事が 10 位になりました。既存ツールを整理した上で、新しいライブラリを開発しています。

tech-blog.abeja.asia

11 位: 85 クリック(#161)

11 位は、特徴量エンジニアリングの技法 Target Encoding でのスムージングに関して、既存ライブラリでの実装を解説している記事。論文の内容も踏まえて紹介しています。

blog.amedama.jp

11 位: 85 クリック(#201)

同率の 11 位に、Kaggle 参画に向けたアドバイスをまとめた記事が入りました。登録・提出・メダル獲得などの話題を扱っています。

qiita.com

13 位: 82 クリック(#193)

今年公開された Python のパッケージ管理ツール rye に関する記事が 13 位に入りました。Kaggle と同様の開発環境構築に取り組んでいます。

zenn.dev

14 位: 80 クリック(#194)

14 位は、今年 10 月に出版された『The Kaggle Workbook 著名コンテストに学ぶ!競技トップレベルの思考と技術』(インプレス)。今年 2 月刊行の『The Kaggle Book:データ分析競技 実践ガイド&精鋭31人インタビュー』の続編です。

https://amzn.asia/d/68J3hIs

14 位: 80 クリック(#202)

同じく 14 位は、今年 10 月開催の「関西Kaggler会 交流会 in Osaka 2023#3」の発表資料がランクイン。特徴量の重要度を用いた特徴選択での注意点を検証しています。

『極意本』サンプルコードをクラウド上で動かそう

Kaggle Advent Calendar 2023 の 1 日目の記事です。

「『極意本』サンプルコードをクラウド上で動かそう」の題目で、11 月 26 日開催の「Kaggle Tokyo Meetup 2023」で発表しました。 会場&サポート提供による Google のスポンサーセッションにお招きいただき、ユーザ視点で Google 関連のクラウドサービスを紹介しました。 発表資料とアーカイブ動画も公開済みですが、本記事では発表の要点をまとめて説明します。

www.youtube.com

発表の題材

発表の題材は、2023 年に刊行した『Kaggleに挑む深層学習プログラミングの極意』(講談社)のサンプルコードです。 GPU 付きのクラウド環境を JupyterNotebook / JupyterLab と共に手軽に構築できる選択肢を解説しました。 取り上げたクラウドサービスは、Kaggle Notebooks / Google Colab / Vertex AI Workbench です。

GitHub のサンプルコード

GitHub では、サンプルコードを JupyterNotebook / JupyterLab から実行できるファイルも提供しています。

!git clone https://github.com/smly/kaggle-book-gokui.git
%cd kaggle-book-gokui/chapter2
!pip install -r requirements.txt
!python 00_mlp.py

動かし方は①リポジトリのダウンロード③ファイルの実行③ファイルの実行ーーと単純ですが、いくつか詰まりがちな箇所があります。 たとえば、CPU / GPU の性能(メモリ含む)、ストレージのサイズ、ライブラリのバージョン(CUDA 含む)などです。

これらの需要に対処するため、3 つの選択肢を紹介しました。 ぜひ、ご自身の用途に合わせた選択をしていきましょう。

Kaggle Notebooks

Kaggle 上で提供されている計算資源です。 2023 年 11 月時点では、毎週標準で GPU を 30 時間、TPU を 20 時間まで利用可能となっています。 2020 年に、需要に応じ週次で追加時間が設けられる仕組みが導入されました。 2022 年の更新では、RAM が 30 GB になり、GPU で P100 か T4*2 を選択可能になりました。 高頻度(最近は毎週)で Kaggle Notebooks を構成するための Docker イメージが更新されています。 バージョンの固定も可能です。

Google Colab

ブラウザ上の実行環境です。 Google Drive を簡単にマウントできる利点があります。 さまざまな課金プランが提供されています。 2023 年 11 月、サイドバーに環境変数設定できるようになりました。

VertexAI Workbench

JupyterLab を独自の設定で起動できる Google Cloud サービスです。 PyTorch や CUDA のバージョンを指定し環境構築できます。 「Kaggle Python [BETA]」という、Kaggle と同等の選択肢もあります。 コア数やメモリも設定できる他、課金額の見積もりが出るのも嬉しいです。

言語処理100本ノック 2020「89. 事前学習済み言語モデルからの転移学習」

問題文

nlp100.github.io

問題の概要

BERT から転移学習します。この章のこれまでの実装と繋がりがなくなりますが、Transformers ライブラリの Trainer を使います。

import os

import datasets
import evaluate
import numpy as np
import pandas as pd
from transformers import (AutoModelForSequenceClassification, AutoTokenizer,
                          DataCollatorWithPadding, Trainer, TrainingArguments)


def preprocess_function(examples):
    return tokenizer(examples["text"], truncation=True, max_length=512)


def compute_metrics(eval_pred):
    logits, labels = eval_pred
    predictions = np.argmax(logits, axis=-1)
    return metric.compute(predictions=predictions, references=labels)


if __name__ == "__main__":

    metric = evaluate.load("accuracy")
    tokenizer = AutoTokenizer.from_pretrained("bert-base-uncased")

    df = pd.read_table(f'ch06/train.txt', header=None)
    df.columns = ["text", "label"]
    valid = pd.read_table(f'ch06/valid.txt', header=None)
    valid.columns = ["text", "label"]
    test = pd.read_table(f'ch06/test.txt', header=None)
    test.columns = ["text", "label"]

    train_dataset = datasets.Dataset.from_pandas(df[["text", "label"]])
    train_tokenized = train_dataset.map(preprocess_function, batched=True)
    val_dataset = datasets.Dataset.from_pandas(valid[["text", "label"]])
    val_tokenized = val_dataset.map(preprocess_function, batched=True)
    test_dataset = datasets.Dataset.from_pandas(test[["text"]])
    test_tokenized = test_dataset.map(preprocess_function, batched=True)

    data_collator = DataCollatorWithPadding(tokenizer=tokenizer)

    model = AutoModelForSequenceClassification.from_pretrained(
        "bert-base-uncased", num_labels=4
    )

    training_args = TrainingArguments(
        output_dir=f"./results",
        learning_rate=4e-5,
        per_device_train_batch_size=4,
        per_device_eval_batch_size=64,
        num_train_epochs=3,
        weight_decay=0.01,
        evaluation_strategy="steps",
        eval_steps=250,
        load_best_model_at_end=True,
        save_steps=1000,
        gradient_accumulation_steps=3,
        save_total_limit=3,
    )

    trainer = Trainer(
        model=model,
        args=training_args,
        train_dataset=train_tokenized,
        eval_dataset=val_tokenized,
        tokenizer=tokenizer,
        data_collator=data_collator,
        compute_metrics=compute_metrics,
    )

    trainer.train()

    oof_results = trainer.predict(test_dataset=val_tokenized)
    np.save(f"oof_prediction", oof_results.predictions)

    results = trainer.predict(test_dataset=test_tokenized)
    np.save(f"test_prediction", results.predictions)

言語処理100本ノック 2020「88. パラメータチューニング」

問題文

nlp100.github.io

問題の概要

何かしらのパラメータをチューニングします。

# ref: https://www.shoeisha.co.jp/book/detail/9784798157184
import re
from collections import defaultdict

import joblib
import pandas as pd
import torch
from gensim.models import KeyedVectors
from torch import nn, optim
from torch.utils.data import DataLoader, Dataset
from tqdm import tqdm


def cleanText(text):
    remove_marks_regex = re.compile("[,\.\(\)\[\]\*:;]|<.*?>")
    shift_marks_regex = re.compile("([?!])")
    # !?以外の記号の削除
    text = remove_marks_regex.sub("", text)
    # !?と単語の間にスペースを挿入
    text = shift_marks_regex.sub(r" \1 ", text)
    return text


def list2tensor(token_idxes, max_len=20, padding=True):
    if len(token_idxes) > max_len:
        token_idxes = token_idxes[:max_len]
    n_tokens = len(token_idxes)
    if padding:
        token_idxes = token_idxes + [0] * (max_len - len(token_idxes))
    return torch.tensor(token_idxes, dtype=torch.int64), n_tokens


class RNN(nn.Module):
    def __init__(self, num_embeddings,
                 embedding_dim=300,
                 hidden_size=300,
                 output_size=1,
                 num_layers=1,
                 dropout=0.2):
        super().__init__()
        # self.emb = nn.Embedding(num_embeddings, embedding_dim,
        #                         padding_idx=0)
        model = KeyedVectors.load_word2vec_format('ch07/GoogleNews-vectors-negative300.bin', binary=True)
        weights = torch.FloatTensor(model.vectors)
        self.emb = nn.Embedding.from_pretrained(weights)
        self.lstm = nn.LSTM(embedding_dim,
                            hidden_size, num_layers,
                            batch_first=True, dropout=dropout, bidirectional=True)
        self.linear = nn.Sequential(
            nn.Linear(hidden_size * 2, 100),
            nn.PReLU(),
            nn.BatchNorm1d(100),
            nn.Linear(100, output_size)
        )

    def forward(self, x, h0=None, n_tokens=None):
        # IDをEmbeddingで多次元のベクトルに変換する
        # xは(batch_size, step_size)
        # -> (batch_size, step_size, embedding_dim)
        x = self.emb(x)
        # 初期状態h0と共にRNNにxを渡す
        # xは(batch_size, step_size, embedding_dim)
        # -> (batch_size, step_size, hidden_dim)
        x, h = self.lstm(x, h0)
        # 最後のステップのみ取り出す
        # xは(batch_size, step_size, hidden_dim)
        # -> (batch_size, 1)
        if n_tokens is not None:
            # 入力のもともとの長さがある場合はそれを使用する
            x = x[list(range(len(x))), n_tokens - 1, :]
        else:
            # なければ単純に最後を使用する
            x = x[:, -1, :]
        # 取り出した最後のステップを線形層に入れる
        x = self.linear(x)
        # 余分な次元を削除する
        # (batch_size, 1) -> (batch_size, )
        # x = x.squeeze()
        return x


class TITLEDataset(Dataset):
    def __init__(self, section='train'):
        X_train = pd.read_table(f'ch06/{section}.txt', header=None)
        use_cols = ['TITLE', 'CATEGORY']
        X_train.columns = use_cols

        d = defaultdict(int)
        for text in X_train['TITLE']:
            text = cleanText(text)
            for word in text.split():
                d[word] += 1
        dc = sorted(d.items(), key=lambda x: x[1], reverse=True)

        words = []
        idx = []
        for i, a in enumerate(dc, 1):
            words.append(a[0])
            if a[1] < 2:
                idx.append(0)
            else:
                idx.append(i)

        self.word2token = dict(zip(words, idx))
        self.data = (X_train['TITLE'].apply(lambda x: list2tensor(
            [self.word2token[word] if word in self.word2token.keys() else 0 for word in cleanText(x).split()])))

        y_train = pd.read_table(f'ch06/{section}.txt', header=None)[1].values
        self.labels = y_train

    @property
    def vocab_size(self):
        return len(self.word2token)

    def __len__(self):
        return len(self.labels)

    def __getitem__(self, idx):
        data, n_tokens = self.data[idx]
        label = self.labels[idx]
        return data, label, n_tokens


def eval_net(net, data_loader, device='cpu'):
    net.eval()
    ys = []
    ypreds = []
    for x, y, nt in data_loader:
        x = x.to(device)
        y = y.to(device)
        nt = nt.to(device)
        with torch.no_grad():
            y_pred = net(x, n_tokens=nt)
            # print(f'test loss: {loss_fn(y_pred, y.long()).item()}')
            _, y_pred = torch.max(y_pred, 1)
            ys.append(y)
            ypreds.append(y_pred)
    ys = torch.cat(ys)
    ypreds = torch.cat(ypreds)
    print(f'test acc: {(ys == ypreds).sum().item() / len(ys)}')
    return


if __name__ == "__main__":
    device = torch.device('cuda:0' if torch.cuda.is_available() else 'cpu')
    batch_size = 640
    train_data = TITLEDataset(section='train')
    train_loader = DataLoader(train_data, batch_size=batch_size,
                            shuffle=True, num_workers=4)
    test_data = TITLEDataset(section='test')
    test_loader = DataLoader(test_data, batch_size=batch_size,
                            shuffle=False, num_workers=4)

    net = RNN(train_data.vocab_size + 1, num_layers=2, output_size=4)
    net = net.to(device)

    loss_fn = nn.CrossEntropyLoss()
    optimizer = optim.SGD(net.parameters(), lr=0.01)

    for epoch in tqdm(range(10)):
        losses = []
        net.train()
        for x, y, nt in train_loader:
            x = x.to(device)
            y = y.to(device)
            nt = nt.to(device)
            y_pred = net(x, n_tokens=nt)
            loss = loss_fn(y_pred, y.long())
            net.zero_grad()
            loss.backward()
            optimizer.step()
            losses.append(loss.item())
            _, y_pred_train = torch.max(y_pred, 1)
            # print(f'train loss: {loss.item()}')
            # print(f'train acc: {(y_pred_train == y).sum().item() / len(y)}')
        eval_net(net, test_loader, device)

言語処理100本ノック 2020「87. 確率的勾配降下法によるCNNの学習」

問題文

nlp100.github.io

問題の概要

RNN で確率的勾配降下法を用いて学習した 言語処理100本ノック 2020「82. 確率的勾配降下法による学習」 - u++の備忘録 と同様です。

# ref: https://www.shoeisha.co.jp/book/detail/9784798157184
import re
from collections import defaultdict

import joblib
import pandas as pd
import torch
from gensim.models import KeyedVectors
from torch import nn, optim
from torch.utils.data import DataLoader, Dataset
from tqdm import tqdm


def cleanText(text):
    remove_marks_regex = re.compile("[,\.\(\)\[\]\*:;]|<.*?>")
    shift_marks_regex = re.compile("([?!])")
    # !?以外の記号の削除
    text = remove_marks_regex.sub("", text)
    # !?と単語の間にスペースを挿入
    text = shift_marks_regex.sub(r" \1 ", text)
    return text


def list2tensor(token_idxes, max_len=20, padding=True):
    if len(token_idxes) > max_len:
        token_idxes = token_idxes[:max_len]
    n_tokens = len(token_idxes)
    if padding:
        token_idxes = token_idxes + [0] * (max_len - len(token_idxes))
    return torch.tensor(token_idxes, dtype=torch.int64), n_tokens


class CNN(nn.Module):
    def __init__(self, num_embeddings,
                 embedding_dim=300,
                 hidden_size=300,
                 output_size=1,
                 kernel_size=3):
        super().__init__()
        # self.emb = nn.Embedding(num_embeddings, embedding_dim,
        #                         padding_idx=0)
        model = KeyedVectors.load_word2vec_format('ch07/GoogleNews-vectors-negative300.bin', binary=True)
        weights = torch.FloatTensor(model.vectors)
        self.emb = nn.Embedding.from_pretrained(weights)
        self.content_conv = nn.Sequential(
            nn.Conv1d(in_channels=embedding_dim,
                      out_channels=hidden_size,
                      kernel_size=kernel_size),
            nn.ReLU(),
            nn.MaxPool1d(kernel_size=(20 - kernel_size + 1))
        )
        self.linear = nn.Linear(hidden_size, output_size)

    def forward(self, x):
        x = self.emb(x)
        content_out = self.content_conv(x.permute(0, 2, 1))
        reshaped = content_out.view(content_out.size(0), -1)
        x = self.linear(reshaped)
        return x


class TITLEDataset(Dataset):
    def __init__(self, section='train'):
        X_train = pd.read_table(f'ch06/{section}.txt', header=None)
        use_cols = ['TITLE', 'CATEGORY']
        X_train.columns = use_cols

        d = defaultdict(int)
        for text in X_train['TITLE']:
            text = cleanText(text)
            for word in text.split():
                d[word] += 1
        dc = sorted(d.items(), key=lambda x: x[1], reverse=True)

        words = []
        idx = []
        for i, a in enumerate(dc, 1):
            words.append(a[0])
            if a[1] < 2:
                idx.append(0)
            else:
                idx.append(i)

        self.word2token = dict(zip(words, idx))
        self.data = (X_train['TITLE'].apply(lambda x: list2tensor(
            [self.word2token[word] if word in self.word2token.keys() else 0 for word in cleanText(x).split()])))

        y_train = pd.read_table(f'ch06/{section}.txt', header=None)[1].values
        self.labels = y_train

    @property
    def vocab_size(self):
        return len(self.word2token)

    def __len__(self):
        return len(self.labels)

    def __getitem__(self, idx):
        data, n_tokens = self.data[idx]
        label = self.labels[idx]
        return data, label, n_tokens


def eval_net(net, data_loader, device='cpu'):
    net.eval()
    ys = []
    ypreds = []
    for x, y, nt in data_loader:
        x = x.to(device)
        y = y.to(device)
        nt = nt.to(device)
        with torch.no_grad():
            y_pred = net(x)
            # print(f'test loss: {loss_fn(y_pred, y.long()).item()}')
            _, y_pred = torch.max(y_pred, 1)
            ys.append(y)
            ypreds.append(y_pred)
    ys = torch.cat(ys)
    ypreds = torch.cat(ypreds)
    print(f'test acc: {(ys == ypreds).sum().item() / len(ys)}')
    return


if __name__ == "__main__":
    device = torch.device('cuda:0' if torch.cuda.is_available() else 'cpu')
    batch_size = 640
    train_data = TITLEDataset(section='train')
    train_loader = DataLoader(train_data, batch_size=batch_size,
                            shuffle=True, num_workers=4)
    test_data = TITLEDataset(section='test')
    test_loader = DataLoader(test_data, batch_size=batch_size,
                            shuffle=False, num_workers=4)

    net = CNN(train_data.vocab_size + 1, output_size=4)
    net = net.to(device)

    loss_fn = nn.CrossEntropyLoss()
    optimizer = optim.SGD(net.parameters(), lr=0.01)

    for epoch in tqdm(range(10)):
        losses = []
        net.train()
        for x, y, nt in train_loader:
            x = x.to(device)
            y = y.to(device)
            nt = nt.to(device)
            y_pred = net(x)
            loss = loss_fn(y_pred, y.long())
            net.zero_grad()
            loss.backward()
            optimizer.step()
            losses.append(loss.item())
            _, y_pred_train = torch.max(y_pred, 1)
            # print(f'train loss: {loss.item()}')
            # print(f'train acc: {(y_pred_train == y).sum().item() / len(y)}')
        eval_net(net, test_loader, device)

言語処理100本ノック 2020「86. 畳み込みニューラルネットワーク (CNN)」

問題文

nlp100.github.io

問題の概要

CNN を実装します。なお実装時には『現場で使える!PyTorch開発入門 深層学習モデルの作成とアプリケーションへの実装』(翔泳社)のサンプルコードを一部流用しました。

# ref: https://www.shoeisha.co.jp/book/detail/9784798157184
import re
from collections import defaultdict

import joblib
import pandas as pd
import torch
from gensim.models import KeyedVectors
from torch import nn, optim
from torch.utils.data import DataLoader, Dataset
from tqdm import tqdm


def cleanText(text):
    remove_marks_regex = re.compile("[,\.\(\)\[\]\*:;]|<.*?>")
    shift_marks_regex = re.compile("([?!])")
    # !?以外の記号の削除
    text = remove_marks_regex.sub("", text)
    # !?と単語の間にスペースを挿入
    text = shift_marks_regex.sub(r" \1 ", text)
    return text


def list2tensor(token_idxes, max_len=20, padding=True):
    if len(token_idxes) > max_len:
        token_idxes = token_idxes[:max_len]
    n_tokens = len(token_idxes)
    if padding:
        token_idxes = token_idxes + [0] * (max_len - len(token_idxes))
    return torch.tensor(token_idxes, dtype=torch.int64), n_tokens


class CNN(nn.Module):
    def __init__(self, num_embeddings,
                 embedding_dim=300,
                 hidden_size=300,
                 output_size=1,
                 kernel_size=3):
        super().__init__()
        # self.emb = nn.Embedding(num_embeddings, embedding_dim,
        #                         padding_idx=0)
        model = KeyedVectors.load_word2vec_format('ch07/GoogleNews-vectors-negative300.bin', binary=True)
        weights = torch.FloatTensor(model.vectors)
        self.emb = nn.Embedding.from_pretrained(weights)
        self.content_conv = nn.Sequential(
            nn.Conv1d(in_channels=embedding_dim,
                      out_channels=hidden_size,
                      kernel_size=kernel_size),
            nn.ReLU(),
            nn.MaxPool1d(kernel_size=(20 - kernel_size + 1))
        )
        self.linear = nn.Linear(hidden_size, output_size)

    def forward(self, x):
        x = self.emb(x)
        content_out = self.content_conv(x.permute(0, 2, 1))
        reshaped = content_out.view(content_out.size(0), -1)
        x = self.linear(reshaped)
        return x


class TITLEDataset(Dataset):
    def __init__(self, section='train'):
        X_train = pd.read_table(f'ch06/{section}.txt', header=None)
        use_cols = ['TITLE', 'CATEGORY']
        X_train.columns = use_cols

        d = defaultdict(int)
        for text in X_train['TITLE']:
            text = cleanText(text)
            for word in text.split():
                d[word] += 1
        dc = sorted(d.items(), key=lambda x: x[1], reverse=True)

        words = []
        idx = []
        for i, a in enumerate(dc, 1):
            words.append(a[0])
            if a[1] < 2:
                idx.append(0)
            else:
                idx.append(i)

        self.word2token = dict(zip(words, idx))
        self.data = (X_train['TITLE'].apply(lambda x: list2tensor(
            [self.word2token[word] if word in self.word2token.keys() else 0 for word in cleanText(x).split()])))

        y_train = pd.read_table(f'ch06/{section}.txt', header=None)[1].values
        self.labels = y_train

    @property
    def vocab_size(self):
        return len(self.word2token)

    def __len__(self):
        return len(self.labels)

    def __getitem__(self, idx):
        data, n_tokens = self.data[idx]
        label = self.labels[idx]
        return data, label, n_tokens


if __name__ == "__main__":
    device = torch.device('cuda:0' if torch.cuda.is_available() else 'cpu')
    batch_size = 640
    train_data = TITLEDataset(section='train')
    train_loader = DataLoader(train_data, batch_size=batch_size,
                            shuffle=True, num_workers=4)

    net = CNN(train_data.vocab_size + 1, output_size=4)
    net = net.to(device)

    for epoch in tqdm(range(10)):
        net.train()
        for x, y, nt in train_loader:
            x = x.to(device)
            y = y.to(device)
            nt = nt.to(device)
            y_pred = net(x)