nikkie-ftnextの日記

イベントレポートや読書メモを発信

transformersのTFBertModelを使ってテキストを特徴量に変換し、ロジスティック回帰、ランダムフォレスト、MLPで分類を試しました

はじめに

頑張れば、何かがあるって、信じてる。nikkieです。
2019年12月末から自然言語処理のネタで毎週1本ブログを書いています。

そこで直近1クール(2020年3月末まで)は、自然言語処理のネタで毎週1本ブログを書くことにします。

今回で最終回を迎えます。
前回3/22の取り組みで「BERTの学習が遅いために、テキストから特徴量を作るのに使われる」ということを体感しました。
その続きとしてBERTで特徴量を作るのを試しました。

目次

動作環境

$ sw_vers
ProductName:    Mac OS X
ProductVersion: 10.14.6
BuildVersion:   18G3020
$ python -V  # venvによる仮想環境を利用
Python 3.7.3
$ pip list  # 手動で入れたものを抜粋して記載 (3/22の環境に追加)
ipython                  7.13.0
numpy                    1.18.1
scikit-learn             0.22.2.post1
tensorflow               2.1.0
transformers             2.5.1

作ったスクリプトたち

ファイル配置

.
├── bbc-text.csv  # 原データ(BBCニューステキスト。5カテゴリ)
├── bert_feature.py
├── bert-feature.csv
├── env  # 仮想環境
├── preprocess.py  # 前処理
├── preprocessed-bbc.csv
└── train_from_bert_feature.py

これらは以下の関係にあります。

  1. preprocess.pybbc-text.csvのテキストを前処理(トークン化)し、preprocessed-bbc.csvとして保存(詳しくは前回の記事を参照)
  2. bert_feature.pypreprocessed-bbc.csvのテキストを特徴量(小数値)に変換し、bert-feature.csvとして保存
  3. train_from_bert_feature.pybert-feature.csvを用いていくつかの分類器を学習

BERTで特徴量を作るにあたっての参考資料

やりたかったことに近かった以下の記事を参考にしました(「ツイートを文章ベクトルに変換する」の部分)。

記事ではPyTorchで実装されていますが、

  1. BertTokenizerでテキストをIDに変換1
  2. BertModel__call__を呼び出し、IDを変換し、文章ベクトルを取り出す

という手順になるようです。

ですが、BertTokenizerBertModelTensorFlowならTFBertModel)を使った他のコードを見てもいまいちピンとこず。。
そんな中で参考になったのが以下の記事(☆)。

DistilBERT(?)についての記事ですが、入出力についてはBERTにも該当するようです。
BERTで特徴量を作るには、BERTのTokenizerやモデルへの入出力の意味を掴むのが早道でした。
たくさんある図を参考にして手を動かしていきました。
記事(☆)に登場するコードの全容はこちら

1.transformersのサンプルコードを理解する

前回「動いた!」と喜んだサンプルコードですが、これが何をしているかの理解が必要でした。

In [1]: import tensorflow as tf

In [2]: from transformers import BertTokenizer, TFBertModel

In [3]: tokenizer = BertTokenizer.from_pretrained('bert-base-uncased')

In [4]: model = TFBertModel.from_pretrained('bert-base-uncased')

BertTokenizerを使ってテキストを対応するIDの並びに変える

BertTokenizerencodeメソッド2にテキスト(str)を渡すと、自然数からなるリストが返ります3

Converts a string in a sequence of ids (integer), using the tokenizer and vocabulary.

この自然数トークンに対応するIDです。
transformersのドキュメントではInput IDと呼ばれています。

In [5]: encoded = tokenizer.encode("Hello, my dog is cute", add_special_tokens=True)

In [6]: encoded
Out[6]: [101, 7592, 1010, 2026, 3899, 2003, 10140, 102]

encodeメソッドのadd_special_tokens=Trueという指定により、文頭や文末を表す[CLS][SEP]に対応するIDも付与されています。

TFBertModelにIDの並びを入力し、出力を得る

TFBertModel__call__メソッドを呼び出して、出力を得ます4

ここで、__call__メソッドへの入力はtf.Tensorにする必要があります。
encodeメソッドの出力を直接与えられません。
tf.constantで変換して渡す必要があります。

また、入力するtf.Tensorの形式は(batch_size, sequence_length)とする必要があります。
この例の場合は1つの文だけなので、batch_sizeが1、sequence_lengthlen(encoded)と同じ8になります。
(batch_size, sequence_length)という2次のテンソルにするために、tf.constant([encoded])という書き方5が必要でした。

In [7]: input_ids = tf.constant([encoded])

In [8]: input_ids.shape
Out[8]: TensorShape([1, 8])

In [10]: outputs = model(input_ids)

TFBertModelの出力から、特徴量を取り出す

__call__メソッドで得たoutputsですが、これは長さ2のtupleでした。

  • output[0]__call__メソッドのドキュメントによるとlast_hidden_state。形式は(batch_size, sequence_length, hidden_size)6
  • output[1]__call__メソッドのドキュメントによるとpooler_output。形式は(batch_size, hidden_size)

output[0](last_hidden_state)からテキストの特徴量が取り出せるようです7注意:(☆)の記事とは変数の対応を変えています)。

In [11]: last_hidden_states = outputs[0]

In [12]: last_hidden_states.shape
Out[12]: TensorShape([1, 8, 768])

(☆)の記事では、文頭を表す[CLS]を表現したテンソルに興味があるとのことなので、それにならって特徴量を取り出します8(理由が腑に落ちていないので深堀りたいところです)。
numpyメソッドでnumpy.ndarrayとして取り出せます。

In [9]: last_hidden_states[:, 0, :].numpy().shape
Out[9]: (1, 768)

こうしてtransformersのサンプルコードの場合は、どのようにすればBERTを使って特徴量が取り出せるのかが分かりました。

なお、以下のIssueも参考になりました。
word or sentence embedding from BERT model · Issue #1950 · huggingface/transformers · GitHub

2.BERTを使ってBBCニュースのテキストを特徴量に変換する

前回使ったBBCニュースのテキスト(2225件)をBERTで特徴量(小数からなるテンソル)に変換します。
コード(bert_feature.py)はこちら:

import csv

import tensorflow as tf
from transformers import BertTokenizer, TFBertModel


tokenizer = BertTokenizer.from_pretrained('bert-base-uncased')
model = TFBertModel.from_pretrained('bert-base-uncased')

categories = ['tech', 'business', 'sport', 'entertainment', 'politics']
category_to_id = {
    category: index for index, category in enumerate(categories)
}
with open('preprocessed-bbc.csv') as fin:
    reader = csv.reader(fin)
    texts_by_ids = []
    category_ids = []
    for text, category in reader:
        texts_by_ids.append(tokenizer.encode(text, max_length=512))
        category_ids.append(category_to_id[category])

max_len = 0
for input_id_list in texts_by_ids:
    if len(input_id_list) > max_len:
        max_len = len(input_id_list)
padded_texts_by_ids = [
    input_id_list + [0]*(max_len-len(input_id_list))  # 0 padding (テキスト長さ揃える)
    for input_id_list in texts_by_ids]

with open('bert-feature.csv', 'a') as fout:
    writer = csv.writer(fout)
    for text_by_ids, category_id in zip(padded_texts_by_ids, category_ids):
        input_ids = tf.constant([text_by_ids])
        output = model(input_ids)
        last_hidden_states = output[0]
        feature = last_hidden_states[:, 0, :].numpy()  # [CLS]についての全重み
        writer.writerow(list(feature[0]) + [category_id])

このコードにより、ニュース1つ1つを768の数値からなる1次のテンソルに変換できました。
達成する中でつまづいたのは以下です。

  • encodeメソッドにmax_length=512と指定する必要があった
    • BertConfigのデフォルト値がモデルに渡っているらしい
    • model.configという辞書を確認したところ、'max_position_embeddings'の値は512だった
    • encodeメソッドのmax_length指定により、512語を超えるテキストでもIDに変換されるトークンは512に揃う(先頭から512語が使われている?)
  • 2225件のテキストを一度にテンソルに変換しようとしたところ、メモリが足りなくなって落ちた(Killedの表示)
    • 1件ずつ変換してCSVファイルに書き込む方法に変更(writer.writerow(list(feature[0]) + [category_id])
    • featureは2次のテンソルのため、[0]指定が必要
    • 1行だけ作ったところ10KB程度(768個のfloat32)。2000件のテキストでは20MB程度のCSVになります
    • 30分くらいかかったように思います(休憩していました)

3.BERTで作った特徴量をもとに分類器を作成

以下のアルゴリズムを試します:

  • sklearn.LogisticRegression
  • sklearn.RandomForestClassifier
  • MLPtf.keras.Sequentialで実装)

Accuracyを比較しました:

Accuracy of LogisticRegression: 0.9285393258426966
Accuracy of RandomForestClassifier: 0.835505617977528
Epoch 1/30
1780/1780 [==============================] - 0s 227us/sample - loss: 1.5421 - accuracy: 0.3444 - val_loss: 1.2829 - val_accuracy: 0.5685
: (略)
Epoch 30/30
1780/1780 [==============================] - 0s 44us/sample - loss: 0.2793 - accuracy: 0.9084 - val_loss: 0.2423 - val_accuracy: 0.9438

RandomForestClassifierよりLogisticRegressionとMLPのAccuracyが高いという結果になりました。
MLPは、validationデータのAccuracyの方が高いため、30epochでは学習不足なようです(epochを50まで増やしたところ学習不足は変わりませんでした。データが少ないため?)

import csv

import numpy as np
from sklearn.ensemble import RandomForestClassifier
from sklearn.linear_model import LogisticRegression
from sklearn.model_selection import cross_val_score
import tensorflow as tf
from tensorflow.keras import layers


with open('bert-feature.csv') as fin:
    reader = csv.reader(fin)
    rows = [row for row in reader]

features = [list(map(float, row[:-1])) for row in rows]
X = np.array(features)
category_ids = [int(row[-1]) for row in rows]
y = np.array(category_ids)

lr = LogisticRegression(
    multi_class='multinomial', solver='saga', max_iter=100
)
rf = RandomForestClassifier()

lr_scores = cross_val_score(lr, X, y, cv=5, scoring='accuracy')
print(f'Accuracy of LogisticRegression: {lr_scores.mean()}')

rf_scores = cross_val_score(rf, X, y, cv=5, scoring='accuracy')
print(f'Accuracy of RandomForestClassifier: {rf_scores.mean()}')

# ref: https://qiita.com/ftnext/items/ff9e08e4686d76eddd40
number_of_classes = 5
y = tf.keras.utils.to_categorical(y, number_of_classes)
model = tf.keras.Sequential(
    [
        layers.Dense(128, input_shape=(768,), activation=tf.nn.relu),
        layers.Dropout(0.5),
        layers.Dense(number_of_classes, activation=tf.nn.softmax),
    ]
)
model.compile(
    loss="categorical_crossentropy",
    optimizer=tf.keras.optimizers.Adam(),
    metrics=["accuracy"],
)
history = model.fit(
    X,
    y,
    batch_size=32,
    epochs=30,
    verbose=1,
    validation_split=0.2,
)

感想

前回にネタとして挙げた「BERTで特徴量を作ってニューラルネットワークを学習」を達成できました!
BERTで特徴量を作るのにも時間はかかりましたが、一度作って保存しておけば色々なモデルで試せるんですね。
これが「初手BERT時代」。。
現在の状況のキャッチアップに少し手がかかってよかったです🤗(まだまだ高い崖がそびえていますが「これからこれから」ですね)

世はまさにPyTorch時代といった感じで、TensorFlowからBERTを使う情報は少ない印象です。
ですが、手を動かす中で、「特徴量作成は結果をファイルに保存するわけだからTensorFlowでなくてもいい、つまり、情報の多いPyTorchでやってもいい」という気づきを得ました。

自然言語処理ネタで週1ブログはこれで終わりです。
この試みはとてもよくて継続したいのはやまやまなのですが、次のクールは別のことを優先する予定です。
取り組みから離れる前に、できたこと、できなかったことをはっきりさせたいので、振り返り記事を予定しています。


  1. encodeメソッドのドキュメントに「Same as doing self.convert_tokens_to_ids(self.tokenize(text))」という記載を見つけ、腑に落ちました。

  2. BertTokenizerPreTrainedTokenizerを継承しており、encodeメソッドはPreTrainedTokenizerに定義されています。

  3. (☆)の図 https://jalammar.github.io/images/distilBERT/bert-distilbert-tokenization-2-token-ids.png が分かりやすいです

  4. (☆)の図 https://jalammar.github.io/images/distilBERT/bert-model-input-output-1.png が分かりやすいです

  5. transformersのドキュメントでは、2次のテンソルにするために [None, :] というインデックス指定がされているようです(この書き方についてドキュメントで裏付けが取れていません)。tf.constantのドキュメントを確認し、2次元配列(リストを要素とするリスト)を渡せば、2次のテンソルとなることが分かりました(a = np.array([[1, 2, 3], [4, 5, 6]]))。

  6. サンプルコードは一文だけのため、sequence_lengthは文に含まれるトークンの数と等しくなります。複数の文がある場合は各文でトークンの数を揃えるために、パディング埋めする必要があるようです。

  7. (☆)の図 https://jalammar.github.io/images/distilBERT/bert-output-tensor.png

  8. (☆)の図 https://jalammar.github.io/images/distilBERT/bert-output-tensor-selection.png

transformersのBERTをTensorFlowからいじって多クラス分類しようとしたところ、ハマった末に😫、BERTは特徴量生成に使うのがよさそうと体験しました🤗

はじめに

頑張れば、何かがあるって、信じてる。nikkieです。
2019年12月末から自然言語処理のネタで毎週1本ブログを書いています。

3/9の週はもろもろ締切が重なりやむなく断念。

3/8の記事で「次回はいまのNLPへのキャッチアップに踏み出す」ことにしていたので、今回はその続きでBERTを触りました。

BERTでテキスト分類をするのが今回手を動かす中での目標でした。

目次

動作環境

$ sw_vers
ProductName:    Mac OS X
ProductVersion: 10.14.6
BuildVersion:   18G3020
$ python -V  # venvによる仮想環境を利用
Python 3.7.3
$ pip list  # 手動で入れたものを抜粋して記載
gensim                   3.8.1
ipython                  7.13.0
tensorflow               2.1.0
transformers             2.5.1

ファイル配置

.
├── bbc-text.csv  # 原データ
├── bert_classify.py
├── env  # 仮想環境
├── preprocess.py  # 前処理
└── preprocessed-bbc.csv

データセットと前処理

データセットはこちらの記事1英語テキストを使います:

これはBBCニュースのテキストで、スポーツ、ビジネスなどのカテゴリが付与されています。
カテゴリは全部で5つあり、その中の1つが付与されるため、問題設定は多クラス分類となります2

上記の記事にgensimを使った前処理があったので、それを使いました(NLTKでもできそうですよね。宿題です)

import csv

import gensim.parsing.preprocessing as gsp


filters = [
    gsp.strip_tags,
    gsp.strip_punctuation,
    gsp.strip_multiple_whitespaces,
    gsp.strip_numeric,
    gsp.remove_stopwords,
    gsp.strip_short,
    gsp.stem_text
]


def clean_text(text):
    text = text.lower()
    for f in filters:
        text = f(text)
    return text


if __name__ == '__main__':
    with open('bbc-text.csv') as fin:
        reader = csv.DictReader(fin)
        rows = list(reader)
    texts = [clean_text(row['text']) for row in rows]
    categories = [row['category'] for row in rows]

    with open('preprocessed-bbc.csv', 'w') as fout:
        writer = csv.writer(fout)
        contents = list(zip(texts, categories))
        writer.writerows(contents)

原データのテキストを前処理し、単語の原形を半角スペースでつないだ状態にします。
前処理に使っている処理は以下の通りです(gensim.parsing.preprocessingドキュメント):

  1. str.lower()で小文字に揃える
  2. gsp.strip_tagsでHTMLタグを除く
  3. gsp.strip_punctuationでpunctuations(句読点)を空白に変えて除く
  4. gsp.strip_multiple_whitespacesで繰り返された空白を除き、空白文字(タブや改行)を空白に揃える
  5. gsp.strip_numericで整数・数値を除く
  6. gsp.remove_stopwordsストップワードを除く(例えば ‘and’, ‘to’, ‘the’ などが除かれる)
  7. gsp.strip_shortで指定した長さ(デフォルトでは3)より短い語を除く
  8. gsp.stem_textでステミング3

ステミングは、単語から語幹を取り出すことです。
これは単語から活用を除くと考えられます。
gsp.stem_textで使っているPorterステマーの副作用か、語尾のeが消えがちでした。

TensorFlowからBERTを使う 🤗

huggingface/transformersを使いました4(もう定跡感がありますね)。

テキスト分類はTFBertForSequenceClassificationを使えばよさそうと分かり、まずはドキュメントのExampleを試します。

In [1]: import tensorflow as tf

In [2]: from transformers import BertTokenizer, TFBertForSequenceClassification

In [3]: tokenizer = BertTokenizer.from_pretrained('bert-base-uncased')

In [4]: model = TFBertForSequenceClassification.from_pretrained('bert-base-uncased')

In [5]: input_ids = tf.constant(tokenizer.encode("Hello, my dog is cute", add_special_tokens=True))[None, :]

In [7]: outputs = model(input_ids)

In [8]: outputs
Out[8]: (<tf.Tensor: shape=(1, 2), dtype=float32, numpy=array([[0.27946013, 0.17132862]], dtype=float32)>,)

BERTで予測までできました!
'bert-base-uncased' という指定ですが、これはS3にアップロードされている単語のリスト5のどれを取得するかなどを指定しているようです。

ここから、上記のBBCのデータをBERTで分類しようとしたのですが、だいぶハマりました 😫

ハマった:TFBertForSequenceClassificationに多クラス分類をやらせるには?

Exampleの TensorFlow 2.0 Bert models on GLUE を元にBBCニュースの多クラス分類を試しました。

examples/run_tf_glue.py を参考にします6

たどり着いたコード

動作しますが、多クラス分類を断念しました。

import csv

import tensorflow as tf
from transformers import (
    BertTokenizer,
    glue_convert_examples_to_features,
    TFBertForSequenceClassification,
)


BUFFER_SIZE = 10000  # データは2000件程度(超えていればシャッフルには十分)
BATCH_SIZE = 32
EVAL_BATCH_SIZE = BATCH_SIZE * 2
EPOCHS = 1  # BERTを学習させるので激重

tokenizer = BertTokenizer.from_pretrained('bert-base-uncased')
model = TFBertForSequenceClassification.from_pretrained('bert-base-uncased')

categories = ['tech', 'business']  # SST-2は二値分類タスクなのでカテゴリを絞る
# categories = ['tech', 'business', 'sport', 'entertainment', 'politics']
category_to_id = {
    category: index for index, category in enumerate(categories)
}
with open('preprocessed-bbc.csv') as fin:
    reader = csv.reader(fin)
    rows = [{
        'idx': index,
        'sentence': row[0],
        'label': category_to_id[row[1]],
    } for index, row in enumerate(reader) if row[1] in categories]

datasets = tf.data.Dataset.from_generator(
    lambda: rows,
    {'idx': tf.int64, 'sentence': tf.string, 'label': tf.int64}
)
datasets.shuffle(BUFFER_SIZE)

train_count = int(len(rows) * 0.8)
valid_count = len(rows) - train_count

train_dataset = datasets.skip(valid_count).take(train_count)
valid_dataset = datasets.take(valid_count)

train_dataset = glue_convert_examples_to_features(
    train_dataset, tokenizer, 128, 'sst-2')
valid_dataset = glue_convert_examples_to_features(
    valid_dataset, tokenizer, 128, 'sst-2')

train_dataset = train_dataset.batch(BATCH_SIZE)
valid_dataset = valid_dataset.batch(EVAL_BATCH_SIZE)

opt = tf.keras.optimizers.Adam(learning_rate=3e-5, epsilon=1e-08)
loss = tf.keras.losses.SparseCategoricalCrossentropy(from_logits=True)
metric = tf.keras.metrics.SparseCategoricalAccuracy('accuracy')

model.compile(optimizer=opt, loss=loss, metrics=[metric])

train_steps = train_count // BATCH_SIZE  # 整数商を求める
valid_steps = valid_count // EVAL_BATCH_SIZE

history = model.fit(
    train_dataset,
    epochs=EPOCHS,
    steps_per_epoch=train_steps,
    validation_data=valid_dataset,
    validation_steps=valid_steps,
)

上記のコードに至る道

元のコードがtf.data.Datasetを使っているようだったので、1月に触ったときの記事を見返しながらもくもく。


まず詰まったのは、BertTokenizerでテキストを整数の並びにエンコードする処理をどこで適用するのかということ。
以下の記事(☆)を見つけ、glue_convert_examples_to_featuresというメソッドを使うことに気づきました。

ところがglue_convert_examples_to_featuresを使ってもエラー(TypeError: tuple indices must be integers or slices, not str)。
ソースを見たり、上記の記事を見返したりしたところ、原因は(1)指定するtaskの値と(2)tf.data.Datasetの形式と判明。

taskはサンプルコードの'mrpc'がテキスト分類ではなかったので、(☆)をもとに'sst-2'(テキストの2クラス分類)を設定7
その後、スタックトレースを元にコードを見たところ、tf.data.Datasetの形式が違う(辞書だ!)と気づきます。

class Sst2Processor(DataProcessor):
    """Processor for the SST-2 data set (GLUE version)."""

    def get_example_from_tensor_dict(self, tensor_dict):
        """See base class."""
        return InputExample(
            tensor_dict["idx"].numpy(),
            tensor_dict["sentence"].numpy().decode("utf-8"),
            None,
            str(tensor_dict["label"].numpy()),
        )

ref: transformers/glue.py at v2.5.1 · huggingface/transformers · GitHub

(☆)を見返して冒頭で辞書を作っていることを確認(読み飛ばしていました)、
from_generatorを使って辞書を元にtf.data.Datasetを作るコード(以下に引用)がドキュメントにあったと思い出し、それを適用。

# Each element is a dictionary mapping strings to `tf.Tensor` objects. 
elements =  ([{"a": 1, "b": "foo"}, 
              {"a": 2, "b": "bar"}, 
              {"a": 3, "b": "baz"}]) 
dataset = tf.data.Dataset.from_generator( 
    lambda: elements, {"a": tf.int32, "b": tf.string}) 

するとエラーがIndexError: list index out of rangeに変わります。
'sst-2'というタスク設定が2クラス分類だからではと思い至り、他クラス分類を断念
2つのカテゴリを選んで、2クラス分類を試します。
すると、

f:id:nikkie-ftnext:20200322213800p:plain

動いたあああああ!!🤗

$ python -i bert_classify.py
Train for 22 steps, validate for 2 steps
22/22 [==============================] - 302s 14s/step - loss: 0.3840 - accuracy: 0.8438 - val_loss: 0.1248 - val_accuracy: 0.9531

感想

ハマりながら粘ってようやく動かせた(学習させられた)BERTなのですが、学習がものすっっっっごく遅いです。😢
学習の進み具合を目にして、BERTで作った特徴量をもとにニューラルネットワークで学習するというアプローチがなぜ取られるのか悟りました。
BERTは優秀なモデルですが、よほどのマシンスペックがないと学習がボトルネックなのですね。
たしかにBERTを特徴量を作る役割で使ったほうがよさそうです(試行錯誤が繰り返せそうですね)。

TensorFlowからTFBertForSequenceClassificationを使って多クラス分類に取り組む事例を探しても全然見つけられなかったのですが、情報がないのは皆さんやらないからなんですね。
このアプローチは筋が悪かったです。

ただ、業務でやっていたと思うとゾッとする経験をしたということで、今週の取り組みに価値はあったと思います。
もし手元のマシンでBERTを学習させてみたいという方は、この記事のコードを手元で動かしてみてください 笑

週1ブログは3月末までとしていたので、来週でいったん最終回を迎えます。
来週のネタは迷っています。
NLTKで触りきれていないところもありますし、BERTで特徴量を作ってニューラルネットワークを学習する方法も知りたいですし。
「終わりよければ全てよし」となるよう、どちらかでまずアウトプットが来週の目標です。

おまけ

BERTのキャッチアップにはWantedlyさんのPodcastがよかったです。
BERTを中心に自然言語処理モデルについてバーっと解説してみた with agatan by Pod de Engineer • A podcast on Anchor


  1. この記事自体写経の題材でした。今回はトピックの違いから盛り込めなかったのですが、またの機会にアウトプット予定です

  2. ライブドアニュースデータセットのイギリス版といった趣ですね

  3. 元の記事にならって8つの処理を順番に適用しましたが、2〜8はpreprocess_stringメソッドにまとまっているようです。gensim恐ろしい子

  4. 導入にあたり、先日発売された『機械学習・深層学習による自然言語処理入門』の10章を参考にしました

  5. transformers.tokenization_bert — transformers 2.5.0 documentationPRETRAINED_VOCAB_FILES_MAP

  6. TensorFlow Blogに記事がありました:Hugging Face: State-of-the-Art Natural Language Processing in ten lines of TensorFlow 2.0 — The TensorFlow Blog

  7. 2018年の言語モデル概要 - LINE ENGINEERING の一覧にも助けられました

イベントレポート | みんなのPython勉強会#55 のオンライン開催にスタッフ参加しました #stapy

はじめに

頑張れば、何かがあるって、信じてる。nikkieです。
2020/03/11にみんなのPython勉強会がオンライン開催されました。
スタッフの1人として会場でバタバタ動いていたのですが、得がたい経験をしたと思っているので、今後に活かすためにも書き留めます。

目次

勉強会の概要

【オンライン開催】みんなのPython勉強会#55 - connpass

「みんなのPython勉強会」では、Pythonを中心としてプログラミングを仕事、研究、趣味など様々なシーンに生かす方法を一緒に学びます。

毎月新橋(リーディング・エッジ社さん)で開催しているPythonの勉強会です。
コロナウイルス感染拡大を受けてオンライン開催に切り替わりました。

新型コロナウィルスの感染が拡大しております。これを受けて、3/11に開催を予定していた本イベントの現地開催(オンサイト開催)を中止し、Youtubeライブ配信を使ったオンライン開催に変更します。当初のプログラムも変更して、StapyコミュニティメンバーによるLT大会とします。

勉強会の様子

Togetter

YouTube アーカイブ

2 〜stapy #55 Online〜

オンライン開催の舞台裏(2時間)をタイムライン形式で共有します。

当初はYouTube Liveで配信する想定でした(前日予行練習もしていました)。
アーキテクチャZoom(登壇者・スタッフのみ)→YouTube Live(参加者)です1
ところが。。。

  • (18:40)スタッフが新橋の会場に集合。画面共有のチェック
  • (19:00)Zoom→YouTube Liveで配信しようとするもエラー発生
  • (19:05)YouTube Liveの代わりにZoomを使うよう転換(登壇者に加えて参加者にも参加していただく。後ほど別のZoomが登場するので、以後Zoom1と表記する)

  • (19:15)Zoom1で stapy #55 スタート
  • (19:20過ぎ)Zoom1のアカウント上限100人に到達

  • (19:30頃)YouTube Liveエラー解消。ただしZoom1と連携していないのでスライドが映らない、配信端末越しに鏡文字として映る状態

  • (19:50頃)会議室のプロジェクターをYouTube Liveに流すことでスライドが映らない問題に対応。これを別のZoom2(登壇者なし)から流して鏡文字も解決?

  • (19:59)Zoom1でもYouTubeでもNaoYさんのデモが見られた!🎉

  • (20:10)リーディング・エッジ社さんからのお知らせをYouTube Liveのみで流してしまう😱。Zoom1参加者の方を放置してしまった(YouTube Liveを案内したが混乱を招いた)

  • (20:30過ぎ)会議室備え付けのマイクを使ってZoom1から流れる音声をZoom2に回していたが、Zoom1を流す端末とZoom2の端末を近づけて置く(マイクを使わない)というソリューションに至る。nikkieの環境(4G & イヤホン)ではYouTubeの音声は多少クリアに
  • (20:50過ぎ)ラップアップ。オンライン懇親会へ
  • (番外。オンライン懇親会で)zoom「nikkieは背景」2

感想

想定外の立ち上がりをし、2時間終始バタバタしていた感があります。
でもめちゃくちゃ楽しかったです。
PCでZoom1を見たり、スマホYouTube Liveを見たり、Twitter実況したりとあっという間に時間が過ぎました。

反省点が多々あるオンライン開催となりましたが、「やって失敗する」は「失敗を懸念してやらない」より望ましいことだと思うので、失敗を修正して今後に繋げられたらと1スタッフとして思います。
(※これは一個人としての見解であり、みんなのPython勉強会としての見解ではありません。もう少しお待ち下さい)

登壇してくださった発表者の皆さま、温かく見守っていただいた参加者の皆さま、誠にありがとうございました。

トークから(抜粋)

資料はZoomでリンクが共有されていたほか、 【オンライン開催】みんなのPython勉強会#55 - 資料一覧 - connpass に上がっています。

以下は抜粋です。

フロントエンド素人がDjango Girls TutorialにVue.jsを使ってみた話 by nao_yさん

Django Girls Tutorialで作るブログアプリにVue.jsを使ってマークダウンで書ける機能を追加したというLT。

  • 先日見つけたDjango MarkdownX - Django Markdownx より使いやすそうな印象
    • Vue.jsを覚えればもっといろいろできそう
    • 少しだけ経験のある(ただし完全に理解はまだしていない)jQueryで色々いじるよりはVue.jsをびゅーっと使えるようになったほうがいいのでは?
  • テンプレートに直接HTMLタグを書くのでなくて静的ファイルとして扱う方法、気になります

PythonスクリプトGUIをつける方法 by 岡崎さん

Eelの例にVue.jsが!

stapy#55 を経て、私の中でVue.jsの存在感が大きくなりました。

1ヶ月で未経験からどこまで上達できるのか by 仙石さん

ザッカーバーグを輩出するためのふるい落とし形式のブートキャンプ。
できる人に絞って著しく伸ばすという点はなかなかないように思われ、興味深いです。

PyCon JP 2020開催に向けての活動紹介 by 副座長 齋藤さん

スタッフ活動がもたらすものについては、かつてのstapyのこちらの資料が参考になると思います。

仲間がいなければ、イベントスタッフになり仲間を作る(スライド17)

紹介していないLTも素晴らしかったので、よろしければ 【オンライン開催】みんなのPython勉強会#55 - 資料一覧 - connpass を見てみてください。
この記事は以上です。

『入門 自然言語処理』6章に取り組み、NLTKだけでも機械学習の分類問題にアプローチできることを知りました

はじめに

頑張れば、何かがあるって、信じてる。nikkieです。
2019年12月末から自然言語処理のネタで毎週1本ブログを書いています。

2/3の週から自然言語処理の基礎固めとして『入門 自然言語処理』に取り組んでいます。

入門 自然言語処理

入門 自然言語処理

今週は、6章「テキスト分類の学習」に取り組みました。

6章は以下で公開されています:

目次

動作環境

先週までと同じ環境を引き続き使っていきます(macOSのアップデートによりBuildVersionが変わりました)。

$ sw_vers
ProductName:    Mac OS X
ProductVersion: 10.14.6
BuildVersion:   18G3020
$ python -V  # venvによる仮想環境を使用
Python 3.7.3
$ pip list  # grepを使って抜粋して表示
beautifulsoup4   4.8.2
ipython          7.12.0
matplotlib       3.1.3
nltk             3.4.5
wordcloud        1.6.0

自然言語処理機械学習

自然言語処理の問題設定には、機械学習における分類1として扱えるものがあります。
例えば、

  • 名前が与えられた時に、男性か女性か分類する
  • 映画のレビューを肯定的か否定的か分類する
  • 品詞タグ付け(トークンが与えられた時に、品詞(名詞、動詞など)を分類する)
  • 文の分割(セグメンテーション) 句読点を文が終了するかどうかで分類する

などです。

NLTKと機械学習

分類の問題設定に使える実装は、NLTKのnltk.classifyパッケージに用意されています。
分類器(classifier)のクラスに用意されたtrainメソッドを使って、データを学習させた分類器を作成します。

分類器のクラスは、次の5つが定義されています2(太字のものが6章で言及されています)。

  • ConditionalExponentialClassifier
  • DecisionTreeClassifier
  • MaxentClassifier
  • NaiveBayesClassifier
  • WekaClassifier

NLTKで分類に取り組む際の流れ

『入門 自然言語処理』では feature という語を素性と訳します。
訳注にありましたが、featureは特徴量とも訳される語だそうです(p.241)。
私には特徴量という呼び方のほうが馴染みがあるので、素性は特徴量に読み替えました。

分類に取り組む時、ゴールは分類器の作成です。

  1. 入力に素性(特徴量)抽出器(feature extractor)を適用して特徴量を取り出す
    • 分類に関連する特徴量を決定
    • 特徴量の符号化方法を決定
  2. 特徴量とラベルから分類器を作成

1と2を1回行っただけで高性能な分類器が作られることはまれです。
分類器の誤りをもとに、手順1から再度取り組むこと(エラー分析)が有益だと知りました。

エラー分析を行うためにはデータの分割がポイントになります3

  • 開発セット
    • 訓練セット 分類器の学習に使う
    • 検証セット エラー分析に使う
  • テストセット 分類器が知らないデータで性能を評価する

データの分割について

開発セットとテストセットを分けることにはトレードオフがあります。

  • 開発セットのデータが少なければ、十分に学習した分類器はできません
  • テストセットのデータが少なければ、分類器の汎用的な性能は分かりません

6.3には、ラベル付けされた大量のデータが利用可能な場合は、全データの10%をテストデータとして使うのが一般的とありました(p.257)。

また、ある文書から開発セットにもテストセットにもデータを分けるべきではありません4
開発セットに使う文書とテストセットに使う文書というように文書単位で分けます(同様に、開発セットの中で、訓練セットに使う文書と検証セットに使う文書も分けます)。

🙆OKな例(開発セットとテストセットで文書を分ける5

In [3]: from nltk.corpus import brown

In [4]: file_ids = brown.fileids(categories='news')

In [5]: len(file_ids)
Out[5]: 44

In [6]: type(file_ids)
Out[6]: list

In [7]: file_ids[:3]
Out[7]: ['ca01', 'ca02', 'ca03']

In [8]: size = int(len(file_ids) * 0.1)

In [9]: size
Out[9]: 4

In [10]: train_set = brown.tagged_sents(file_ids[size:])

In [11]: test_set = brown.tagged_sents(file_ids[:size])

In [12]: len(train_set)
Out[12]: 4227

In [13]: len(test_set)
Out[13]: 396

file_idsをshuffleしてもよさそうですね。

🙅NGな例(tagged_sentsを使ったことでリーケージの懸念あり)

In [1]: import random

In [2]: random.seed(42)

In [14]: tagged_sents = list(brown.tagged_sents(categories='news'))  # NGなので真似しないでください

In [15]: random.shuffle(tagged_sents)

In [16]: size = int(len(tagged_sents) * 0.1)

In [17]: size
Out[17]: 462

In [18]: train_set, test_set = tagged_sents[size:], tagged_sents[:size]

In [19]: len(train_set)
Out[19]: 4161

In [20]: len(test_set)
Out[20]: 462

名前が男性か女性かの分類に取り組む

それでは、例題として名前が男性か女性かの分類に取り組んでみます。
アルファベットの名前が与えられた時に、男性の名前か女性の名前かを分類します。

  • namesコーパスを用います
  • 名前の最後の1文字を特徴にします
  • NaiveBayesClassifierを扱います(後ほどDecisionTreeClassifier、MaxentClassifierも試します)
  • 書籍に沿ってエラー分析を実施し、特徴量抽出器を更新します

namesコーパス

In [21]: from nltk.corpus import names

In [29]: print(names.readme())
Names Corpus, Version 1.3 (1994-03-29)
Copyright (C) 1991 Mark Kantrowitz
Additions by Bill Ross

This corpus contains 5001 female names and 2943 male names, sorted
alphabetically, one per line.

# 省略

Mark Kantrowitz <mkant+@cs.cmu.edu>
http://www-2.cs.cmu.edu/afs/cs/project/ai-repository/ai/areas/nlp/corpora/names/

In [30]: names = [(name, 'male') for name in names.words('male.txt')] + [(name, 'female') for name in names.words('female.txt')]

In [31]: len(names)
Out[31]: 7944

特徴量抽出 & データの分割

In [32]: random.shuffle(names)

In [33]: def gender_features(word):  # 最後の1文字を取り出す
    ...:     return {'last_letter': word[-1]}
    ...:

In [35]: names[:3]
Out[35]: [('Raye', 'female'), ('Marita', 'female'), ('Fey', 'female')]

In [51]: devtest_names = names[500:1500]  # 1000件

In [52]: test_names = names[:500]  # 500件

In [54]: train_names = names[1500:]

In [55]: len(train_names)
Out[55]: 6444

In [57]: train_set = [(gender_features(n), g) for n, g in train_names]

In [58]: devtest_set = [(gender_features(n), g) for n, g in devtest_names]

In [59]: test_set = [(gender_features(n), g) for n, g in test_names]

分類器作成(NaiveBayesClassifier)

nltk.classify.naivebayes.NaiveBayesClassifierから分類器を作ります(仕組みについては6.5で説明されています)。

In [26]: import nltk

In [60]: classifier = nltk.NaiveBayesClassifier.train(train_set)

In [61]: nltk.classify.accuracy(classifier, devtest_set)
Out[61]: 0.766

検証セットにおける正解率は76.6%でした。

分類に有益な特徴量の上位も確認できます。

In [62]: classifier.show_most_informative_features(5)
Most Informative Features
             last_letter = 'a'            female : male   =     31.4 : 1.0
             last_letter = 'f'              male : female =     26.9 : 1.0
             last_letter = 'k'              male : female =     26.8 : 1.0
             last_letter = 'p'              male : female =     11.3 : 1.0
             last_letter = 'd'              male : female =      9.8 : 1.0

エラー分析

In [63]: errors = []

In [65]: for name, tag in devtest_names:
    ...:     guess = classifier.classify(gender_features(name))
    ...:     if guess != tag:
    ...:         errors.append((tag, guess, name))
    ...:

In [66]: len(errors)
Out[66]: 234

In [67]: errors[:5]
Out[67]:
[('female', 'male', 'Marlo'),
 ('female', 'male', 'Hildagard'),
 ('female', 'male', 'Melicent'),
 ('female', 'male', 'Moll'),
 ('male', 'female', 'Georgie')]

エラー分析でどう間違えたかを確認するためnames(名前と教師ラベルの組)を訓練、検証、テストに分けています。
たしかに、この出力を見れば工夫できそうですね。

エラーから分かることの一例:
分類器はhで終わる名前をfemaleと分類するが、chで終わる名前はmaleに分類してほしい

そこで、最後の2文字も特徴量として抽出します。

In [68]: def gender_features(word):
    ...:     return {'suffix1': word[-1:],
    ...:             'suffix2': word[-2:]}
    ...:

In [69]: train_set = [(gender_features(n), g) for n, g in train_names]

In [70]: devtest_set = [(gender_features(n), g) for n, g in devtest_names]

In [72]: classifier = nltk.NaiveBayesClassifier.train(train_set)

In [73]: nltk.classify.accuracy(classifier, devtest_set)
Out[73]: 0.788

In [75]: classifier.show_most_informative_features(5)
Most Informative Features
                 suffix2 = 'na'           female : male   =     87.6 : 1.0
                 suffix2 = 'la'           female : male   =     62.8 : 1.0
                 suffix2 = 'ta'           female : male   =     37.4 : 1.0
                 suffix2 = 'rd'             male : female =     35.3 : 1.0
                 suffix2 = 'ia'           female : male   =     33.7 : 1.0

正解率が78.8%に増加しました。
エラー分析はさらに繰り返せそうです。

なお、エラー分析を繰り返すたびに、訓練セットと検証セットを分け直したほうがいいそうです。
理由は検証セットのデータの偏りを分類器に反映させないためです。

別の分類器を試す

1.決定木(DecisionTreeClassifier)

nltk.classify.decisiontree.DecisionTreeClassifierを試します。

In [76]: classifier = nltk.DecisionTreeClassifier.train(train_set)

In [77]: nltk.classify.accuracy(classifier, devtest_set)
Out[77]: 0.782

決定木は6.4で説明されています。
決定木はフローチャートであり、解釈しやすい場合が多いという特徴があるそうです。

pseudocodeメソッドでフローチャートを文字で確認できます。

In [80]: print(classifier.pseudocode(depth=2))
if suffix2 == 'Ag': return 'female'
if suffix2 == 'Al': return 'male'
if suffix2 == 'Bo': return 'male'
if suffix2 == 'Cy': return 'male'
if suffix2 == 'Di': return 'female'
# 省略

決定木は決定株(1つの特徴量に基づき、分岐を1つだけ持つ決定木)を選ぶ6ことで作られます。
根となる決定株を選び、葉の正解率を調べ、十分な正解率でない場合は葉を決定株で置き換えて、決定木を育てていきます。

解釈しやすいという利点がある決定木ですが、欠点もあります。

  • 特徴量が比較的独立したものである場合であっても、特定の順番で調べることを強制する
  • ラベル付けに関する関与の小さな特徴量を扱うのが得意でない

決定木はフローチャートにせざるを得ないので、特徴量を特定の順番でチェックすることになるのだと理解しました。

なお、単純ベイズ分類器(NaiveBayesClassifier)は、全ての特徴量を「並列に」扱うことで、決定木の問題を克服しているそうです。

2. 最大エントロピー分類器(MaxentClassifier)

nltk.classify.maxent.MaxentClassifierを試します。

In [81]: classifier = nltk.MaxentClassifier.train(train_set)
  ==> Training (100 iterations)

      Iteration    Log Likelihood    Accuracy
      ---------------------------------------

             1          -0.69315        0.367
             2          -0.34214        0.792
      # 省略
            99          -0.30114        0.805
         Final          -0.30113        0.805

In [82]: nltk.classify.accuracy(classifier, devtest_set)
Out[82]: 0.788

最大エントロピー分類器は6.6で説明されています。

  • 単純ベイズ分類器は、モデルのパラメタとして確率を使用
  • 最大エントロピー分類器は、分類器の性能(≒全体尤度)を最大化するパラメタのセットを探す

※理論部分は今回は読めておらず、宿題事項です

試したいこと

  • LazyMapを作るnltk.classify.util.apply_features(メモリ消費を抑える)
  • 名前の判定以外の例:映画レビュー
  • 品詞タグ付け
    • 文脈を利用する
    • 同時分類器の利用(系列分類器)
  • アルゴリズムの部分は『見て試してわかる機械学習アルゴリズムの仕組み 機械学習図鑑』で分かるところから補強
  • 訓練セットと検証セットに交差検証を適用したい(nltkにある?sklearnから使う?)
  • 「recommended machine learning packages that are supported by NLTK」があるとのことなのですが、どんなパッケージがあるのだろう(NLTKのサイトに見つけること能わず)

感想

「NLTKは高機能!」この一言につきます。
これまではステミングやタグ付けなどの自然言語の取り扱い方や、コーパスを使っての集計方法を学んできました。
それだけではなく、scikit-learnにあるような分類器も実装されていたとは!
Web開発におけるDjangoのような"電池同梱"っぷりですね。
書籍で紹介されていた交差検証までNLTKでできるのか、それともscikit-learnに任せたほうがいいのか、NLTKにおける機械学習の限界が気になります。

これまで『入門 自然言語処理』で自然言語処理の基本を見てきました。
「実務のあの部分はもっとうまくできたな」という発見がいくつもありました。

さて、この本が書かれた時点(2009)と現在とでは、自然言語処理を取り巻く状況は異なります。
10年前と比べて機械学習の発展はめざましく、この本には載っていない深層学習を使った文書分類は、各フレームワークチュートリアルにも見られます。
そこで次回は機械学習いまへのキャッチアップを目指し、ついにBERTを触る予定です。
最近出て話題の"あの本"が候補です。


  1. 教師あり学習の分類です(離散値を予測する問題設定のことです)

  2. NLTK HOWTOの Classifiers より

  3. 機械学習のデータの分け方の話と同じです。機械学習で作りたいのは汎用的なモデルです。なので、学習に使っていないデータについてどれだけ正しく分類できるかが、モデルを作成する中で最終的に知りたいことになります

  4. 自然言語処理におけるリーケージとして理解しました

  5. NLTKのコーパスtagged_sentsメソッドは、第1引数にファイルIDを指定できます(これまではファイルIDを指定せずに使ってきました)

  6. 選ぶ方法で一般的な方法が情報利得だそうです(積ん読部分)

イベントレポート | オンラインで開催された #pycon_shizu 、私の知らないPythonがいくつもありました!

はじめに

頑張れば、何かがあるって、信じてる。nikkieです。
2020/02/29にPyCon mini Shizuokaがオンライン開催されました。
そこで聞いたトークのレポートをまとめます。

なお、自分の登壇については別にまとめています。

目次

勉強会の概要

PyCon mini Shizuokaとは静岡のPythonコミュニティによる初めて開催されるイベントです。
静岡のPythonコミュニティは西部、中部、東部と多数ありますが、一堂に会する機会ができないかという思いがあり、この度開催することとなりました。

テーマは「あなたの知らないPython」です。

藤枝での開催に向けて進められてきましたが、コロナウィルス感染症の影響を考慮し、オンライン開催に切り替わりました。

togetterにまとめが上がっています。

nikkieの知らないPython

トーク中のメモツイートを元に PyCon トーク リポジトリを更新しています。

「こんな分野でPythonを使っているのか!」という発見がいくつもありました。
このレポートでは、3つのトピックでレポートします。

  1. MakerとPython(からあげさん、Miyakeさん)
  2. GUIPython(岡崎さん)
  3. 質の高いコードを書くという観点から見たPython(ほぼろさん)

MochizukiさんのJAMstackの話や、みずきさんのテストコードの話も印象的でした。

1. MakerとPython

からあげさん、Miyakeさんのトークは、興味深い作品がいくつも飛び出しました。
どう作っているか気になるので、からあげさんのブログの記事は後ほど拝見します。

ソフトウェアが好きな私ですが、Makerの世界を垣間見てしまうと、ソフトウェアだけでは現実世界に物理的な影響を及ぼせないのが少し物足りなく感じます。
また、ソフトウェアだけまず分かればいいと手を出してこなかったのですが、深層学習でGPUを使おうとすると、ハードウェア周りの知識が求められる印象です(例:cudaって何?ドライバー?)。
なので、どこかのタイミングでハードウェアの世界に踏み出してみたいです(自宅に眠っているラズパイもありますし)。
今回「趣味Tech」という素敵な言葉とも出会えました!

PyCon mini Shizuokaで見た中では、Miyakeさんのライブハックが強烈に刺さりました。

自分の好きとMaker活動が組合せられるとわかったので、推し風Botを現実世界に召喚と夢が膨らみます🤗

2. GUIPython

岡崎さんによるGUIアプリケーションについてのトーク
tkinterPySimpleGUIを比較し、後者が簡単と紹介。
PySimpleGUIで作ったGUIアプリが次々と登場しました。

懇親会では、最近話題になったアスキーアートの記事GUIで実行できるようにしたコードを披露していただき、CLIかWebアプリかという2択しか持たなかった私は「GUIって結構有効な選択肢かも」と気づきました。
アスキーアートの例はWebアプリにしようとすると画像の扱いが大変そうという印象なのですが、GUIであればWebアプリほど難しくなさそう、だから魅力的と考えています(サーバにアップロードするわけではないので)

サンプルコードを動かしてみようと思っています。

3. 質の高いコードを書くという観点から見たPython

ほぼろさんのトークは私には一番刺さりました!
ほぼろさんがCLIツールを作る過程をトレースする中で、これまでの独学で出会わなかった考え方と出会い、非常に勉強になりました。

  • .gitignorehttp://gitignore.io にて「PythonとVirtualenv」を指定して生成
  • CLIのテンプレートを利用(何もしないmain関数)動作確認できる状態をまず作る
  • ファイルの切り分け(main.pyはユーザとのやり取りに限り、CSVを作るのに必要な処理は別のパッケージに
  • パッケージを作るときはモジュールをパッケージ外にどう見せるかを考える
  • dataclassfrozen=True(初期化後に変更されない)
  • モックとして要素1のリスト(Iterableであればforを使って何個であっても同じように扱える)
  • リストよりジェネレータを優先的に使う」(メモリ効率からジェネレータがオススメ)
  • 仮想環境でのパッケージ管理:requirements.txtとrequirements.lockの2つを作る
    • 手で入れたパッケージは.txtに
    • pip freezeの出力結果は.lockに
  • パッケージの中に定数をまとめるconstant.py

これら以外にも学びがあり、自分のツイートをまとめています。

このソースコードは2回は写経して共有いただいたことを自分のものにします!
t-wadaさんが『テスト駆動開発』に書いている「ケント・ベックとのペアプロ」ってこういうことか!と思いました。

感想

PyCon mini Shizuokaは「あなたの知らないPython」というテーマそのもので、Pythonの知らなかった一面をいくつも見ることができました
ハードウェア系の発表が多かったのも特色だと思います。
東海工業地帯という土地柄もあり、静岡・愛知はMakerの地なのかもしれませんね。

次回はオフライン開催を目指すということで、次回開催を楽しみにしています。
オンライン懇親会では「食はさわやか」と聞けました😋
沼津はラブライブ!サンシャイン!!の聖地のほか、沼津港が有名だそうです。

PyCon mini Shizuokaの運営スタッフ、登壇者、参加者の皆さま、ありがとうございました!

登壇報告 | 2/29にオンラインで開催されたPyCon mini ShizuokaでDjango入門トークをしました #pycon_shizu

はじめに

頑張れば、何かがあるって、信じてる。nikkieです。
2020/02/29にPyCon mini ShizuokaでDjango入門トークをしました。
その報告といただいた感想や登壇準備について振り返ります。

目次

勉強会の概要

PyCon mini Shizuokaとは静岡のPythonコミュニティによる初めて開催されるイベントです。
静岡のPythonコミュニティは西部、中部、東部と多数ありますが、一堂に会する機会ができないかという思いがあり、この度開催することとなりました。

テーマは「あなたの知らないPython」です。

藤枝での開催に向けて進められてきましたが、コロナウィルス感染症の影響を考慮し、オンライン開催に切り替わりました。

togetterにまとめが上がっています。

発表報告

Web開発に興味がある方を対象に「DjangoでWeb開発を始めよう」とオススメするトークをしました。
Djangoには様々なチュートリアルがありますが、Django Girls Tutorialを題材に、あらかじめ知っていたらチュートリアルに取り組みやすくなると私が考える情報を提供しました。
例えば

  • プロジェクトとアプリケーションとはなにか
  • URL設定、ビュー、テンプレート、モデルのそれぞれが何か、そしてどう連携しているのか

といった点です1

Djangoは「締切がある完璧主義者向け」なので、多機能である反面、学習コストが高いです。
そんなDjangoの推しポイントは、Webに精通していなくてもDjangoの流儀に反しない限りは、安全で拡張可能なWebアプリが作れる点です。

登壇資料(スライド)

登壇資料のほか、デモアプリと補足資料も用意しました。

デモアプリ

補足資料

いただいた感想から

YouTube Liveのコメントでは、わかりやすかったといった声をいくつもいただきました!
このトークを機にDjangoチュートリアルに取り組んでいただけたら嬉しいです。

聞く方に馴染みのあるWebアプリを使っての説明は工夫した点なので、嬉しいです。

補足説明

Django Girlsについて

Django Girls TutorialはDjango Girlsという女性がプログラミングに出会うワークショップの教材です。
ワークショップの参加は女性限定ですが、Django Girls TutorialはWebに公開されており、どなたでも利用できます
詳しくはスライドのAppendix

似た事例として、PyLadiesの勉強会に男性は参加できないですが、公開された資料は見られます

ASGI

最後に駆け足でしたDjango 3系の話への反応が多かったです2

Appendixにありますが、ASGIについて知るには、PyCon JP 2018の「Djangoだってカンバンつくれるもん」がオススメです。

事前準備

1月の登壇

今回の発表はサポーターズ勉強会での登壇でわかったことを取り入れています3

例えば、Webアプリ開発=ファイルを作ることで、Jupyter Notebookは使わないなどです。
私には当たり前のことだったために見落としていたのですが、Web開発を知らない方には説明する必要がありますね。

サポーターズ勉強会運営、そしてご参加の皆さま、どうもありがとうございました!

また、サポーターズ勉強会でのオンライン登壇の経験も今回活かせました。
冒頭、「ふだんPythonでやっていることをYouTube Liveにコメントしてください。操作の練習だと思って」と伝えました。
これは目の前に相手がいないときは、聞いている方がいることを早めに確認した方が発表しやすいという経験からです4
コメントが現れたのを見て、登壇者がどのくらい時をかけているか(=タイムラグがどのくらいあるか)も分かりました。

発表者練習会

みんなのPython勉強会で聞いた、日本システム技研さんの発表練習にならって、都内から参加する方を誘って、1週間前に練習していました。
ここでのフィードバックを元に3連休で資料を整えています。
ご参加いただいた岡崎さん、ありがとうございました!

デモアプリ開発

今回登壇準備の中で手を動かせたことがよかったと感じています。
精力的に登壇していますが、わかりやすい説明に工数を割いていて、あんまりコードを書いていないなという後ろめたさがありました。

ソースコード

開発したい事項をGitHubProjectに洗い出し、着手したものをIssueに変換して進めていきました。

残った開発事項

終わりに

Django Congress 2019、PyCon JP 2019に続いて3回目のカンファレンス登壇でしたが、3回目にしてようやく準備の進め方が分かってきました
お恥ずかしい話ですが、これまでは準備期間がギリギリで、胃がキリキリする思いをしながら臨んでいました。
今回はこれまでより余裕があり(といっても緊張していましたが)、準備の進め方は今までよりも成功したと思っています。

ひと月くらい前に登壇内容を固めたり、1-2週前には練習会をしてフィードバックを求めるというのは今後も継続していきます。

急遽オンライン開催という転換でしたが、発表の機会を用意してくださったPyCon mini Shizuoka運営の皆さまに感謝申し上げます。
また、発表を聞いてくださった方、TwitterYouTube Liveにコメントしてくださった方、ありがとうございました。

PyCon mini Shizuokaのイベントレポートを別の記事で予定しています。


  1. こちらは Django Girls チュートリアルには足りないものが1つある - 子供の落書き帳 Renaissance への私なりの回答でもあります

  2. 6月に予定されているDjango Congress 2020でASGIはホットな話題となるかもしれないと思いました。プロポーザルは3/15いっぱいまでです!

  3. なお、こちらでORMのパフォーマンスについての質問が出たのですが、開発経験がなく答えられないという経験をしました。Optimize the Django ORM - DEV Community 👩‍💻👨‍💻が参考になるのかなと思っています。小さくてもWebアプリを1つ作って運用というのは、今年の私の目標です(2Qにやろうかな)

  4. サポーターズ勉強会でのオンライン登壇(YouTube Live)は複数回経験していますが、初回は目の前に聞き手がいなくて反応が見えず、必要以上に丁寧に説明してしまい、用意したコンテンツの半分程度しか進められなかったという苦い思い出があります。ただこの経験は次の回以降(、そして今回にも!)活きています

『入門 自然言語処理』12章から、分かち書きした日本語のテキストがNLTKに読み込め、扱いは意外と英語テキストと共通と学びました

はじめに

頑張れば、何かがあるって、信じてる。nikkieです。
2019年12月末から自然言語処理のネタで毎週1本ブログを書いています。

2/3の週から自然言語処理の基礎固めとして『入門 自然言語処理』に取り組んでいます。

入門 自然言語処理

入門 自然言語処理

今週は日本語版限定の12章「Python による日本語自然言語処理」の一部に取り組みました。

  • 日本語のテキストはNLTKでどのように扱うのか知る
  • 形態素解析MeCabを使う

※前提として、週1ブログの取り組みの中で、日本語のテキストはjanomeで扱ってきました1

12章は以下で公開されています:

公開されている12章のコードは書籍と同様にPython2系向けのようです2

目次

動作環境

今週の開始時は先週までと同じ環境です。

$ sw_vers
ProductName:    Mac OS X
ProductVersion: 10.14.6
BuildVersion:   18G103
$ python -V  # venvによる仮想環境を使用
Python 3.7.3
$ pip list  # grepを使って抜粋して表示
beautifulsoup4   4.8.2
ipython          7.12.0
matplotlib       3.1.3
nltk             3.4.5
wordcloud        1.6.0

ファイル配置

.
├── ch12_ja  # 日本語のテキストを配置
├── env
├── # 日本語のWordCloud作成のために置いたフォントファイルなどは省略

扱う日本語テキストの取得

青空文庫から宮沢賢治の『銀河鉄道の夜』を使います。
宮沢賢治 銀河鉄道の夜

書籍では取得が省略されているので、3章の復習がてら手を動かしてみました。
urllibを使ってWebから取得し、HTMLタグを削除します。
エンコーディングがShift JISでした!(UTF-8だと想定していたらUnicodeDecodeErrorで落ちました)

<?xml version="1.0" encoding="Shift_JIS"?>
In [12]: import urllib.request

In [20]: from bs4 import BeautifulSoup

In [21]: with urllib.request.urlopen('https://www.aozora.gr.jp/cards/000081/files/456_15050.html') as f:
    ...:     html_bytes = f.read()
    ...:     html = text_bytes.decode('shift-jis')
    ...:

In [22]: soup = BeautifulSoup(html, 'html.parser')

In [23]: text = soup.get_text()

In [24]: with open('ch12_ja/gingatetsudono_yoru.txt', 'w') as fout:
    ...:     fout.write(text)
    ...:

この方法だとhead要素のtitleタグが残り、先頭に空行が多かったので、bodyタグを指定するのがよさそうです(本と実行結果を比べづらかったので手動で削除しました) 。

分かち書きされていないコーパスの扱い

上で取得した『銀河鉄道の夜』をNLTKのコーパスとして読み込みます。
先週の5章の取り組みではbrownコーパスを使いましたが、ここでは『銀河鉄道の夜』のテキストからgingaコーパスを作ります。

テキストの読み込みにはnltk.corpus.reader.PlaintextCorpusReaderを使います。

イニシャライザに渡す引数は4

の5つです。

In [29]: from nltk import RegexpTokenizer, Text

In [30]: from nltk.corpus.reader import PlaintextCorpusReader

In [31]: from nltk.corpus.reader.util import read_line_block

word_tokenizer, sent_tokenizerにはnltk.tokenize.regexp.RegexpTokenizerを渡します。

sent_tokenizer(文の区切り方を指定)はセリフの「」を考慮した上で、!または?または。で区切っています。

In [32]: jp_sent_tokenizer = RegexpTokenizer(r'[^ 「」!?。]*[!?。]')

word_tokenizer(単語の区切り方を指定)は、連続するひらがな、カタカナ、漢字を単語をすると仮で決めています。
例えば「一つ一つ」が「一/つ/一/つ」と区切られてしまうので、この分け方はあくまで仮のものです。

In [33]: jp_char_tokenizer = RegexpTokenizer(r'([ぁ-んー]+|[ァ-ンー]+|[\u4E00-\u9FFF]+|^[ぁ-んァ-ンー\u4E00-\u9FFF]+)')

コーパスを作りましょう。

In [34]: ginga = PlaintextCorpusReader('ch12_ja', 'gingatetsudono_yoru.txt',
    ...:     encoding='utf-8', para_block_reader=read_line_block,
    ...:     sent_tokenizer=jp_sent_tokenizer, word_tokenizer=jp_char_tokenizer)
    ...:

コーパスからは

  • rawメソッドでPlaintextCorpusReaderに渡したファイルの全文(複数ある場合は連結)
  • wordsメソッドでトークンのリスト

を取得できます。

In [38]: print(ginga.raw()[:50])
銀河鉄道の夜
宮沢賢治




一、午后(ごご)の授業

「ではみなさんは、そういう

In [39]: print('/'.join(ginga.words()[:50]))
銀河鉄道/の/夜/宮沢賢治/一/午后/ごご/の/授業/ではみなさんは/そういうふうに/川/だと/云/い/われたり/乳/の/流/れたあとだと/云/われたりしていたこのぼんやりと/白/いものがほんとうは/何/かご/承知/ですか/先生/は/黒板/に/吊/つる/した/大/きな/黒/い/星座/の/図/の/上/から/下/へ/白/くけぶった/銀河帯

wordsメソッドは5章で使ったbrownにもありました。
NLTKのコーパスとして読み込むことで、英語でも日本語でも扱い方(インターフェース)が揃うと理解しました。

トークンのリストをnltk.text.Textに変換します。

In [40]: ginga_t = Text(w for w in ginga.words())  # ginga_t = Text(ginga.words()) で済むように思われる

In [41]: ginga_t.concordance('川')
Displaying 25 of 57 matches:
 の 夜 宮沢賢治 一 午后 ごご の 授業 ではみなさんは そういうふうに 川 だと 云 い われたり 乳 の 流 れたあとだと 云 われたりしていたこのぼ
がするのでした 先生 はまた 云 いました ですからもしもこの 天 あま の 川 がわ がほんとうに 川 だと 考 えるなら その 一 つ 一 つの 小 さな
# [省略]

concordanceメソッドで指定された単語について、索引を表示しています。

分かち書きされたコーパスの扱い

NLTKに用意されたjeitaコーパスを読み込みます。

In [43]: import nltk

In [45]: nltk.download('jeita')
Out[45]: True

[nltk_data] Downloading package jeita to
[nltk_data]     /Users/.../nltk_data...

/Users/.../nltk_data/corpora/にはjeita.zipがダウンロードされますが、展開されません。
そこでコマンドラインからunzipしました

In [49]: !unzip ~/nltk_data/corpora/jeita.zip -d ~/nltk_data/corpora/jeita

In [50]: !ls ~/nltk_data/corpora/jeita/jeita/
README      a0680.chasen    a1370.chasen    a2060.chasen    g0037.chasen
# [省略]

READMEの内容はこちらでも確認できました:

このコーパスChaSen形式で分かち書きされています。
ChaSen形式とは、各語について

  1. 出現形
  2. 読み
  3. 原形
  4. 品詞
  5. 活用

がタブ区切りで並んだ形式です(後ろの項目は品詞によってはありません)。

In [67]: !head ~/nltk_data/corpora/jeita/jeita/a0010.chase
    ...: n
      記号-空白
新潟  ニイガタ    新潟  名詞-固有名詞-地域-一般
の ノ の 助詞-連体化
停車場   テイシャジョウ   停車場   名詞-一般
を ヲ を 助詞-格助詞-一般
出る  デル  出る  動詞-自立   一段  基本形
と ト と 助詞-接続助詞
列車  レッシャ    列車  名詞-一般
の ノ の 助詞-連体化
箱 ハコ  箱 名詞-一般

読み込むにはnltk.corpus.reader.ChasenCorpusReaderを使います。

In [52]: from nltk.corpus.reader import ChasenCorpusReader

In [55]: jeita = ChasenCorpusReader('/Users/.../nltk_data/corpora/jeita/', '.*chasen', encoding='utf-8')

In [56]: print('/'.join(jeita.words()[22100:22140]))
たい/という/気持/が/、/この上なく/純粋/に/、/この上なく/強烈/で/あれ/ば/、/ついに/は/そのもの/に/なれる/。/なれ/ない/の/は/、/まだ/その/気持/が/そこ/まで/至っ/て/い/ない/から/だ/。/法

分かち書きされており、品詞タグ付け済みのため、5章で扱ったbrownのようにtagged_sentメソッドが使えました。

In [63]: tab = '\t'

In [64]: print('\nEOS\n'.join(['\n'.join(f'{w[0]}/{w[1].split(tab)[2]}' for w in sent) for sent in jeita.tagged_sents()[2170:2171]]))
を/助詞-格助詞-一般
まくっ/動詞-自立
た/助動詞
とき/名詞-非自立-副詞可能
# [省略]

※EOSが出力されなかったので、コードを写し間違えているかもしれません5

MeCabを導入する

12.2で登場するMeCabを導入します。
導入方法は『Pythonによるあたらしいデータ分析の教科書』にならいました。

$ brew install mecab-ipadic  # 依存関係にあるmecabもインストールされる
$ mecab -v
mecab of 0.996

動作確認です。

$ mecab  # 対話的に使います
天気の子
天気  名詞,一般,*,*,*,*,天気,テンキ,テンキ
の 助詞,連体化,*,*,*,*,の,ノ,ノ
子 名詞,一般,*,*,*,*,子,コ,コ
EOS

最新語に対応できるようmecab-ipadic-NEologdも入れました。

$ git clone --depth 1 git@github.com:neologd/mecab-ipadic-neologd.git  # ホームディレクトリで実行しています
$ cd mecab-ipadic-neologd/
$ ./bin/install-mecab-ipadic-neologd -n

先ほどの例を使うと、最新語に対応できているかも確認できます。

$ mecab -d /usr/local/lib/mecab/dic/mecab-ipadic-neologd/
天気の子
天気の子    名詞,固有名詞,一般,*,*,*,天気の子,テンキノコ,テンキノコ
EOS

別の日本語テキストを試す:先人の残したまどマギのセリフ

12章を調べながら取り組んでいたところ、偉大な先人(yutakikuchiさん)の功績を偶然にも発見しました。

ここまでに学んだことから

  1. まどマギのテキストをMeCabChaSen形式にする
  2. ChaSen形式の分かち書きされたテキストをChasenCorpusReaderコーパスとして読み込む
  3. コーパスに含まれるトークンからTextを作るとconcordanceが見られる

んじゃないかと電波を受信し、手を動かしてみました。Let's try!

yutakikuchiさんがリポジトリに残したまどマギのセリフテキストを取得します。

$ wget https://raw.githubusercontent.com/yutakikuchi/NLTK/master/madmagi/madmagi_corpus-euc.txt -O ch12_ja/madomagi-euc.txt

文字コードEUC JPだったので、PythonUTF-8に変換してもらいます。

In [69]: with open('ch12_ja/madomagi-euc.txt', 'rb') as fin, open('ch12_ja/madom
    ...: agi_utf8.txt', 'wb') as fout:
    ...:     euc_bytes = fin.read()
    ...:     euc_text = euc_bytes.decode('euc_jp')
    ...:     utf8_bytes = euc_text.encode()
    ...:     fout.write(utf8_bytes)
    ...:

MeCabChaSen形式にします。
人名に対応できるようにneologdを指定しています。

$ mecab -d /usr/local/lib/mecab/dic/mecab-ipadic-neologd/ -O chasen < ch12_ja/madomagi_utf8.txt > ch12_ja/madomagi.chasen

以上が1です。
では2でNLTKのコーパスとして読み込みましょう。

In [71]: madomagi = ChasenCorpusReader('ch12_ja/', 'madomagi.chasen', encoding='utf-8')

In [73]: madomagi_t = Text(madomagi.words())

concordanceを見てみます。

In [74]: madomagi_t.concordance('助ける')
Displaying 1 of 1 matches:
 … ? 死ん じゃ うって 、 わかっ て た のに … 。 私 なんか 助ける より も 、 あなたに … … 生き て て ほしかっ た のに … あな

In [75]: madomagi_t.concordance('助け')
Displaying 1 of 1 matches:
よ ね うん … キュゥべえ に 騙さ れる 前 の バカ な 私 を 、 助け て あげ て くれ ない か な ? 約束 する わ 。 絶対 に あなた

In [78]: madomagi_t.concordance('いや')
Displaying 1 of 1 matches:
思う ん だ 鹿目 さん … さよなら 。 ほむらちゃん 。 元気 で ね いや ! 行かないで … 鹿目 さ ぁぁぁん ! ! どうして … ? 死ん じ

yutakikuchiさんは

事前の予想では「いやだ」とか「助けて」などの台詞が頻繁に使われていると考えた

と綴っていますが、予想通り「助ける」や「いや」はネガティブなセリフで使われていますね。

あとは、まどマギを象徴するこの語句:

In [80]: madomagi_t.concordance('魔法少女')
Displaying 7 of 7 matches:
変え られる の ? もちろん さ 。 だから 僕 と 契約 し て 、 魔法少女 に なっ て よ ! 私 は 巴マミ あなた たち と 同じ 、 見滝
 、 見滝 原中 の 3 年生 そして キュゥべえ と 契約 し た 、 魔法少女 よ は あー はぁ 。 うん やあ はい 、 これ う わぁ … 。 い
夫だよ 、 ほむらちゃん あ 、 あなた たち は … 彼女たち は 、 魔法少女 。 魔女 を 狩る 者 たち さ いきなり 秘密 が バレ ちゃっ た
 ない ! 鹿目 さん まで 死ん じゃう よ ? それでも 、 私 は 魔法少女 だ から 。 みんな の こと 、 守ら なきゃ いけ ない から ねぇ
 時 、 間に合っ て 。 今 でも それ が 自慢 な の だから 、 魔法少女 に なっ て 、 本当に よかっ た って 。 そう 思う ん だ 鹿目
 は 心臓 の 病気 で ずっと ・ ・ ・ あ 鹿目 さん 、 私 も 魔法少女 に なっ た ん だ よ ! これから 一緒 に 頑張ろ う ね ! え
 られる の ? もちろん さ 。 だから 、 僕 と 契約 し て 、 魔法少女 に なっ て よ ! ダメ ぇぇ ぇぇ ぇぇ ぇぇ ぇぇ ぇぇ ! !

魔法少女は「契約してなるもの」「魔女を狩る者」とわかりますね(まどマギを知らない方がどの程度分かるかは未知数ですが)。

まとめ

日本語テキストの扱いですが、12章を少し読んだところ、以下の方法でNLTKのコーパスとして扱えそうという認識です。

  1. MeCabChaSen形式に分かち書き
  2. NLTKのChasenCorpusReaderコーパスとして読み込む(brownなどのコーパスと同じように扱える)

NLTKにコーパスとして取り込めれば、英語テキストと日本語テキストの扱いにそれほど大きな違いはないというのが暫定的な結論です。

感想

まどマギのテキストの扱いが受信した電波の通りにできたので、「もしかしてどんな日本語テキストでもこの扱いでいける!?」という期待半分、間違っているかもという不安半分という心境です。

不安要素は12章が1-2割程度しか読み進められず、この扱いに見落としがあるかもしれないと思うからです。
時間を見つけて読み進められればと思っています。

今回はconcordanceを使いましたが、Textでできることに他に何があるのか見ておきたいところです(similarityもあったように思います)。
また、5章で見たのと同様に指定した品詞の単語の取り出しもできそうです。

次回は『入門 自然言語処理』6章「テキスト分類の学習」に取り組む予定です。


  1. 例えば、「Janome ではじめるテキストマイニング」の中のWordCloudのチュートリアルに取り組み、janomeを全然使いこなせていなかったと思い知りました - nikkie-ftnextの日記

  2. 公開されている英語版はPython 3系で書き直されています。例 3.3 ch03.rst2

  3. ドキュメントによれば「Unicode 実装で使用される現在のデフォルトエンコーディング名を返」すので、期待通りの結果ですね

  4. ドキュメントにはっきり書いていないので、コードを見ました

  5. tab変数はSyntaxError: f-string expression part cannot include a backslashへの対応です ref: https://stackoverflow.com/a/44780467