CUBE SUGAR CONTAINER

技術系のこと書きます。

Python: TensorFlow/Keras で Word2Vec の CBOW を実装してみる

(2021-02-04 追記): ニューラルネットワークのアーキテクチャで、出力側の Embedding が誤って Dense になっていた部分を修正した。

Word2Vec の CBOW (Continuous Bag-of-Words) は、単語の分散表現 (Word Embedding) を得るために用いられるニューラルネットワークのアーキテクチャのひとつ。 今回は、それを TensorFlow/Keras を使って実装してみた。 なお、ニューラルネットワークのアーキテクチャは、オライリーの「ゼロから作るDeep Learning ❷ ――自然言語処理編」を参考にしている。

ゼロから作るDeep Learning ❷ ―自然言語処理編

ゼロから作るDeep Learning ❷ ―自然言語処理編

  • 作者:斎藤 康毅
  • 発売日: 2018/07/21
  • メディア: 単行本(ソフトカバー)

ただし、あらかじめ断っておくと実用性はそれほどない。 実用上は gensim とかを使った方が学習は速いし、得られる分散表現も良い性能になるはず。

使った環境は次のとおり。

$ sw_vers
ProductName:    macOS
ProductVersion: 11.2
BuildVersion:   20D64
$ python -V       
Python 3.8.7
$ pip list | grep "tensorflow "
tensorflow             2.4.1

もくじ

下準備

あらかじめ、必要なパッケージをインストールしておく。

$ pip install tensorflow gensim scipy

また、学習するデータセットをダウンロードしておく。 今回は PTB (Penn Treebank) をコーパスに用いる。

$ wget https://raw.githubusercontent.com/tomsercu/lstm/master/data/ptb.train.txt
$ wget https://raw.githubusercontent.com/tomsercu/lstm/master/data/ptb.valid.txt

サンプルコード

早速だけど、以下にサンプルコードを示す。 ニューラルネットワークは SimpleContinuousBagOfWords という名前で実装した。 CBOW は、ある単語の左右に出現する単語から、その単語を推定するアーキテクチャになっている。 そのため、ネットワークに供給するデータは左右に出現する 2 つの単語になる。 そして、どんな単語が出るかを One-Hot 形式の予測値として出力している。 肝心の単語埋め込みは、ネットワーク内にある Embedding レイヤーの重み (Weights) として得られる。

#!/usr/bin/env python3
# -*- coding: utf-8 -*-

from __future__ import annotations

import re
from itertools import count
from typing import Iterable
from typing import Iterator
from functools import partial

import numpy as np  # type: ignore
import tensorflow as tf  # type: ignore
from tensorflow.keras import Model  # type: ignore
from tensorflow.keras.layers import Embedding  # type: ignore
from tensorflow import Tensor  # type: ignore
from tensorflow.data import Dataset  # type: ignore
from tensorflow.keras.losses import CategoricalCrossentropy  # type: ignore
from tensorflow.keras.optimizers import Adam  # type: ignore
from tensorflow.keras.layers import Dense  # type: ignore
from tensorflow.keras.layers import Softmax  # type: ignore
from tensorflow.keras.layers import Layer # type: ignore
from tensorflow.keras.models import Sequential  # type: ignore
from tensorflow.keras.callbacks import Callback  # type: ignore
from gensim.test.utils import datapath  # type: ignore
from scipy.stats import pearsonr  # type: ignore


class OutputEmbedding(Layer):
    """出力側の単語埋め込み層"""

    def __init__(self, vocab_size: int):
        super().__init__()
        self.vocab_size = vocab_size

    def build(self, input_shape):
        # 埋め込み次元数
        self.embedding_size = input_shape[1]
        # 出力部分に使うので、通常の Embedding とは shape が転置している
        self.embedding = self.add_weight(shape=(self.embedding_size, self.vocab_size))

    def call(self, inputs, **kwargs):
        # 重みは最初から転置してあるので transpose する必要はない
        # (NxD) * (DxV) = (NxV)
        return tf.tensordot(inputs, self.embedding, axes=1)


class SimpleContinuousBagOfWords(Model):
    """Word2Vec の CBOW モデルを実装したクラス"""

    def __init__(self, vocab_size: int, embedding_size: int):
        super().__init__()

        # 入力側の単語埋め込み層
        self.embedding_layer = Embedding(input_dim=vocab_size,
                                         input_shape=(1, ),
                                         output_dim=embedding_size,
                                         name='word_embedding',
                                         )
        self.output_layer = Sequential([
            OutputEmbedding(vocab_size),
            Softmax(),
        ])

    def call(self, inputs: Tensor) -> Tensor:
        # 推定したい単語の左側にある単語のベクトルを取り出す
        left_context = inputs[:, 0]
        left_vector = self.embedding_layer(left_context)
        # 推定したい単語の右側にある単語のベクトルを取り出す
        right_context = inputs[:, 1]
        right_vector = self.embedding_layer(right_context)
        # 両方の単語に対応するベクトルを足して 2 で割る
        mixed_vector = (left_vector + right_vector) / 2
        # 出力側の Embedding と積を取ってから単語の出現確率にする
        prediction = self.output_layer(mixed_vector)
        return prediction


class WordSimilarity353Callback(Callback):
    """WordSim353 データセットを使って単語間の類似度を評価するコールバック"""

    def __init__(self, word_id_table: dict[str, int]):
        super().__init__()

        self.word_id_table = word_id_table
        self.model = None

        # 評価用データを読み込む
        self.eval_data = []
        wordsim_filepath = datapath('wordsim353.tsv')
        with open(wordsim_filepath, mode='r') as fp:
            # 最初の 2 行はヘッダなので読み飛ばす
            fp.readline()
            fp.readline()
            for line in fp:
                word1, word2, sim_score = line.strip().split('\t')
                self.eval_data.append((word1, word2, float(sim_score)))

    def set_model(self, model):
        self.model = model

    def _cosine_similarity(self, x, y):
        nx = x / np.sqrt(np.sum(x ** 2))
        ny = y / np.sqrt(np.sum(y ** 2))
        return np.dot(nx, ny)

    def on_epoch_end(self, epoch, logs=None):
        # モデルから学習させたレイヤーの重みを取り出す
        model_layers = {layer.name: layer for layer in self.model.layers}
        embedding_layer = model_layers['word_embedding']
        word_vectors = embedding_layer.weights[0].numpy()

        # 評価用データセットに含まれる単語間の類似度を計算する
        labels = []
        preds = []
        for word1, word2, sim_score in self.eval_data:
            # Out-of-Vocabulary な単語はスキップ
            if word1 not in self.word_id_table or word2 not in self.word_id_table:
                continue

            # コサイン類似度を計算する
            word1_vec = word_vectors[self.word_id_table[word1]]
            word2_vec = word_vectors[self.word_id_table[word2]]
            pred = self._cosine_similarity(word1_vec, word2_vec)
            preds.append(pred)
            # 正解ラベル
            labels.append(sim_score)

        # ピアソンの相関係数を求める
        r_score = pearsonr(labels, preds)[0]
        print(f'Pearson\'s r score with WordSim353: {r_score}')


def load_corpus(filepath: str) -> Iterator[str]:
    """テキストファイルからコーパスを読み出す"""
    tag_replace = re.compile('<.*?>')
    with open(filepath, mode='r') as fp:
        for line in fp:
            # コーパスに含まれる <unk> や <eos> は取り除きたい
            yield re.sub(tag_replace, '', line)


def sentences_to_words(sentences: Iterable[str], lower: bool = True) -> Iterator[list[str]]:
    """文章を単語に分割する"""
    for sentence in sentences:
        if lower:
            sentence = sentence.lower()
        words = re.split('\\W+', sentence)
        yield [word for word in words if len(word) > 0]  # 空文字は取り除く


def word_id_mappings(sentences: Iterable[Iterable[str]]) -> dict[str, int]:
    """単語を ID に変換する対応テーブルを作る"""
    counter = count(start=0)

    word_to_id = {}
    for sentence in sentences:
        for word in sentence:

            if word in word_to_id:
                # 登録済みの単語
                continue

            # 単語の識別子を採番する
            word_id = next(counter)
            word_to_id[word] = word_id

    return word_to_id


def words_to_ids(sentences: Iterable[list[str]], word_to_id: dict[str, int]) -> Iterator[list[int]]:
    # 単語を対応する ID に変換する
    for words in sentences:
        # NOTE: Out-of-Vocabulary への対応がない
        yield [word_to_id[word] for word in words]


def cosine_similarity_matrix(word_vectors: np.ndarray, eps: float = 1e-8) -> np.ndarray:
    """単語ベクトル間のコサイン類似度を計算する"""
    word_norm = np.sqrt(np.sum(word_vectors ** 2, axis=1)).reshape(word_vectors.shape[0], -1)
    normalized_word_vectors = word_vectors / (word_norm + eps)
    cs_matrix = np.dot(normalized_word_vectors, normalized_word_vectors.T)
    return cs_matrix


def most_similar_words(word_id: int, cs_matrix: np.ndarray, top_n: int = 5):
    """コサイン類似度が最も高い単語の ID を取り出す"""
    similarities = cs_matrix[word_id, :]
    similar_word_ids = np.argsort(similarities)[::-1]
    # 基本的にコサイン類似度が最も高い ID は自分自身になるはずなので先頭は取り除く
    return similar_word_ids[1: top_n + 1]


def extract_contexts(word_ids: Tensor, window_size: int = 1) -> Tensor:
    """コンテキストの単語をラベル形式で得る"""
    right_contexts = word_ids[window_size + 1:]
    left_contexts = word_ids[:-window_size - 1]
    contexts = tf.transpose([left_contexts, right_contexts])
    return contexts


def extract_targets(word_ids: Tensor, vocab_size: int, window_size: int = 1) -> Tensor:
    """ターゲットの単語を One-Hot 形式にする"""
    targets = word_ids[window_size:-window_size]
    # 正解との損失を計算するために One-Hot 表現にする
    one_hot_targets = tf.one_hot(targets, depth=vocab_size)
    return one_hot_targets


def dataset_pipeline(corpus_words: Iterator[list[str]], word_id_table: dict[str, int]) -> Dataset:
    """ターゲットとコンテキストを供給するパイプライン"""

    # ID に変換したコーパスを行ごとに読み出せるデータセット
    word_ids_ds = Dataset.from_generator(lambda: words_to_ids(corpus_words, word_id_table),
                                         tf.int32,
                                         output_shapes=[None])

    # コンテキストを抽出するパイプライン
    contexts_ds = word_ids_ds.map(extract_contexts,
                                  num_parallel_calls=tf.data.AUTOTUNE,
                                  # ターゲットと対応関係を作る必要があるので決定論的に動作させる
                                  deterministic=True)

    # ターゲットを抽出するパイプライン
    vocab_size = len(word_id_table.keys())
    f = partial(extract_targets, vocab_size=vocab_size)
    targets_ds = word_ids_ds.map(f,
                                 num_parallel_calls=tf.data.AUTOTUNE,
                                 # コンテキストと対応関係を作る必要があるので決定論的に動作させる
                                 deterministic=True)

    # 行単位で処理されているので flatten して結合する
    zipped_ds = Dataset.zip((contexts_ds.unbatch(), targets_ds.unbatch()))
    return zipped_ds


def count_words(corpus_words: Iterator[list[str]]) -> int:
    """コーパスに含まれる単語の数をカウントする"""
    return sum(len(words) for words in corpus_words)


def main():
    # Penn Treebank コーパスを読み込む
    train_sentences = load_corpus('ptb.train.txt')
    valid_sentences = load_corpus('ptb.valid.txt')

    # コーパスを単語に分割する
    train_corpus_words = list(sentences_to_words(train_sentences))
    valid_corpus_words = list(sentences_to_words(valid_sentences))

    # 単語 -> ID
    word_to_id = word_id_mappings(train_corpus_words)
    # コーパスの語彙数
    vocab_size = len(word_to_id.keys())

    # モデルとタスクを定義する
    EMBEDDING_SIZE = 50
    criterion = CategoricalCrossentropy()
    optimizer = Adam(learning_rate=1e-2)
    model = SimpleContinuousBagOfWords(vocab_size, EMBEDDING_SIZE)
    model.compile(optimizer=optimizer,
                  loss=criterion)

    # データセットを準備する
    TRAIN_BATCH_SIZE = 1024
    train_ds = dataset_pipeline(train_corpus_words, word_to_id)
    train_ds = train_ds.shuffle(buffer_size=512)
    train_ds = train_ds.repeat()
    train_ds = train_ds.batch(TRAIN_BATCH_SIZE)
    train_ds = train_ds.prefetch(buffer_size=tf.data.AUTOTUNE)
    # 検証用データはリピートしない
    VALID_BATCH_SIZE = 1024
    valid_ds = dataset_pipeline(valid_corpus_words, word_to_id)
    valid_ds = valid_ds.batch(VALID_BATCH_SIZE)
    valid_ds = valid_ds.prefetch(buffer_size=tf.data.AUTOTUNE)

    # 厳密に要素数を計算するのは面倒なので、ざっくり単語数の 2 倍をエポックということにしておく
    num_of_train_samples = count_words(train_corpus_words) * 2

    callbacks = [
        # WordSim353 データセットを使って単語間の類似度を相関係数で確認する
        WordSimilarity353Callback(word_to_id),
    ]
    # 学習する
    model.fit(train_ds,
              steps_per_epoch=num_of_train_samples // TRAIN_BATCH_SIZE,
              validation_data=valid_ds,
              epochs=5,
              callbacks=callbacks,
              verbose=1,
              )

    # モデルから学習させたレイヤーの重みを取り出す
    model_layers = {layer.name: layer for layer in model.layers}
    embedding_layer = model_layers['word_embedding']
    word_vectors = embedding_layer.weights[0].numpy()

    # 単語を表すベクトル間のコサイン類似度を計算する
    cs_matrix = cosine_similarity_matrix(word_vectors)
    # ID -> 単語
    id_to_word = {value: key for key, value in word_to_id.items()}

    # いくつか似ているベクトルを持った単語を確認してみる
    example_words = ['you', 'year', 'car', 'toyota']
    for target_word in example_words:
        # ID に変換した上で最も似ている単語とそのベクトルを取り出す
        print(f'The most similar words of "{target_word}"')
        target_word_id = word_to_id[target_word]
        most_similar_word_ids = most_similar_words(target_word_id, cs_matrix)
        # 単語とベクトルを表示する
        for rank, similar_word_id in enumerate(most_similar_word_ids, start=1):
            similar_word = id_to_word[similar_word_id]
            similarity = cs_matrix[target_word_id, similar_word_id]
            print(f'TOP {rank}: {similar_word} = {similarity}')
        print('-' * 50)


if __name__ == '__main__':
    main()

上記に名前をつけて保存したら実行してみよう。 CPU を使って学習すると、1 エポックにそれなりの時間がかかるけど、まあなんとかなるレベルのはず。 エポック間には、WordSim353 データセットを使って評価した、単語間の類似度がピアソンの相関係数として表示される。 学習が終わったら、いくつかの単語について、類似したベクトルを持つ単語を上位 5 件について表示させている。

$ python cbow.py

...

1651/1651 [==============================] - 181s 109ms/step - loss: 6.1526 - val_loss: 5.1804
Pearson's r score with WordSim353: 0.21794273571470427
Epoch 2/5
1651/1651 [==============================] - 182s 110ms/step - loss: 4.7424 - val_loss: 5.2353
Pearson's r score with WordSim353: 0.2428972583779604
Epoch 3/5
1651/1651 [==============================] - 172s 104ms/step - loss: 4.4591 - val_loss: 5.3129
Pearson's r score with WordSim353: 0.2583752228928214
Epoch 4/5
1651/1651 [==============================] - 160s 97ms/step - loss: 4.3708 - val_loss: 5.3941
Pearson's r score with WordSim353: 0.25454681209868246
Epoch 5/5
1651/1651 [==============================] - 4038s 2s/step - loss: 4.3027 - val_loss: 5.4816
Pearson's r score with WordSim353: 0.25160892813703517
The most similar words of "you"
TOP 1: we = 0.8296732306480408
TOP 2: they = 0.7543421983718872
TOP 3: i = 0.668086051940918
TOP 4: she = 0.5567612051963806
TOP 5: imbalances = 0.5095677375793457
--------------------------------------------------
The most similar words of "year"
TOP 1: week = 0.7451192140579224
TOP 2: month = 0.7027692794799805
TOP 3: day = 0.6059724688529968
TOP 4: spring = 0.5649460554122925
TOP 5: decade = 0.5465914011001587
--------------------------------------------------
The most similar words of "car"
TOP 1: seed = 0.6106906533241272
TOP 2: taxi = 0.6024419665336609
TOP 3: auto = 0.574679434299469
TOP 4: siemens = 0.5651793479919434
TOP 5: subscriber = 0.5451180934906006
--------------------------------------------------
The most similar words of "toyota"
TOP 1: ford = 0.6328699588775635
TOP 2: celebrity = 0.5823256373405457
TOP 3: humana = 0.5597572922706604
TOP 4: supermarkets = 0.5554667115211487
TOP 5: honda = 0.5467281937599182
--------------------------------------------------

上記をみると、学習データの損失はエポックを重ねるごとに小さくなっているが、検証データの損失は 2 エポック目以降から悪化している。 これは一般的には過学習している状態だが、WordSim353 を使った単語間類似度の評価は 2 エポック目も向上している。 また、ベストなエポックの結果ではないものの、最後に出力している類似の単語については、そこそこ納得感がある結果に思える。

ゼロから作るDeep Learning ❷ ―自然言語処理編

ゼロから作るDeep Learning ❷ ―自然言語処理編

  • 作者:斎藤 康毅
  • 発売日: 2018/07/21
  • メディア: 単行本(ソフトカバー)

Python: 正の相互情報量 (PPMI) と特異値分解 (SVD) を使った単語の分散表現

(2021-02-02 追記): 共起行列の計算を NumPy の Integer array indexing を使った実装にした

オライリーの「ゼロから作るDeep Learning ❷ ――自然言語処理編」を読んでいる。 この中に、カウントベースで計算する初歩的な単語の分散表現が紹介されていて、なかなか面白かった。 単語の分散表現を得る方法は色々とあるけど、カウントベースは単語の出現数を元にした比較的シンプルなもの。 今回は、正の相互情報量 (Positive Pointwise Mutual Information; PPMI) と特異値分解 (Singular Value Decomposition; SVD) を使って単語の分散表現を獲得する例を実装してみた。 書籍に記載されている内容を参考にしつつも、自分なりのコードにしてある。 オリジナルとの差分で大きいのは、正の相互情報量と単語間のコサイン類似度を計算する部分を NumPy のベクトル演算にしたところかな。

使った環境は次のとおり。

$ sw_vers
ProductName:    Mac OS X
ProductVersion: 10.15.7
BuildVersion:   19H114
$ python -V               
Python 3.8.7

もくじ

下準備

あらかじめ、必要なパッケージをインストールしておく。

$ pip install scikit-learn

コーパスは、前述の書籍の中で用いられていた「Penn Treebank (PTB)」を使う。 こちらも、あらかじめダウンロードしておこう。

$ wget https://raw.githubusercontent.com/tomsercu/lstm/master/data/ptb.train.txt

サンプルコード

以下に実装したサンプルコードを示す。 コメントもつけてあるけど、サンプルコードでは次のような流れで処理している。

  • PTB コーパスを読み込む
  • 単語を ID に変換する
    • 語彙 (Vocabulary) を構築する
  • 共起行列を計算する
    • 文章の中で、ある単語と別の単語が共に用いられた回数を表している
      • つまり文の中で近い位置に登場した、ということ
  • 正の相互情報量を計算する
    • ある単語と別の単語が共に用いられる度合いを表している
  • 特異値分解して単語の分散表現を得る
    • 相互情報量を複数の行列に分解すると共に次元圧縮をかける
  • いくつかの単語について類似の分散表現を持った単語を調べる
    • 分散表現の近さをコサイン類似度で測る
#!/usr/bin/env python3
# -*- coding: utf-8 -*-

from __future__ import annotations

import re
from itertools import count
from typing import Iterable
from typing import Iterator

import numpy as np  # type: ignore
from sklearn.utils.extmath import randomized_svd  # type: ignore


def load_corpus(filepath: str) -> Iterator[str]:
    """テキストファイルからコーパスを読み出す"""
    tag_replace = re.compile('<.*?>')
    with open(filepath, mode='r') as fp:
        for line in fp:
            # コーパスに含まれる <unk> や <eos> は取り除きたい
            yield re.sub(tag_replace, '', line)


def sentences_to_words(sentences: Iterable[str], lower: bool = True) -> Iterator[list[str]]:
    """文章を単語に分割する"""
    for sentence in sentences:
        if lower:
            sentence = sentence.lower()
        words = re.split('\\W+', sentence)
        yield [word for word in words if len(word) > 0]  # 空文字は取り除く


def word_id_mappings(sentences: Iterable[Iterable[str]]) -> dict[str, int]:
    """単語を ID に変換する対応テーブルを作る"""
    counter = count(start=0)

    word_to_id = {}
    for sentence in sentences:
        for word in sentence:

            if word in word_to_id:
                # 登録済みの単語
                continue

            # 単語の識別子を採番する
            word_id = next(counter)
            word_to_id[word] = word_id

    return word_to_id


def words_to_ids(sentences: Iterable[list[str]], word_to_id: dict[str, int]) -> Iterator[list[int]]:
    # 単語を対応する ID に変換する
    for words in sentences:
        # NOTE: Out-of-Vocabulary への対応がない
        yield [word_to_id[word] for word in words]


def co_occurence_matrix(corpus_ids: Iterable[list[int]], vocab_size: int, window_size: int) -> np.ndarray:
    """共起行列 (Co-occurence Matrix) を計算する"""
    co_matrix = np.zeros((vocab_size, vocab_size), dtype=np.int32)

    for word_ids in corpus_ids:
        for context_len in range(1, window_size + 1):
            # 元の ID 列と、コンテキスト分をずらした ID 列を用意する
            base = word_ids[context_len:]
            shifted = word_ids[:-context_len]
            # 元の列とずらした列の対応を取れば、共起した単語を Integer array indexing で処理できる
            co_matrix[base, shifted] += 1
            co_matrix[shifted, base] += 1

    return co_matrix


def positive_pointwise_mutual_information(co_matrix: np.ndarray, eps: float = 1e-8):
    # 正の相互情報量 (Positive Pointwise Mutual Information) を計算する
    total_words = np.sum(co_matrix)
    word_count = np.sum(co_matrix, axis=0)

    # PPMI = max(PMI, 0)
    # PMI = log2(P(x, y) / (P(x) * P(y)) = log2(C(x, y) * N / (C(x) * C(y)))
    # P(x) = C(x) / N
    # P(x, y) = C(x, y) / N

    # ベクトル化した計算
    ppmi_matrix = co_matrix.copy().astype(np.float32)
    ppmi_matrix *= total_words
    ppmi_matrix /= word_count * word_count.reshape(word_count.shape[0], -1) + eps
    ppmi_matrix = np.log2(ppmi_matrix + eps)
    ppmi_matrix = np.clip(ppmi_matrix, 0., None)

    return ppmi_matrix


def cosine_similarity_matrix(word_vectors: np.ndarray, eps: float = 1e-8) -> np.ndarray:
    """単語ベクトル間のコサイン類似度を計算する"""
    word_norm = np.sqrt(np.sum(word_vectors ** 2, axis=1)).reshape(word_vectors.shape[0], -1)
    normalized_word_vectors = word_vectors / (word_norm + eps)
    cs_matrix = np.dot(normalized_word_vectors, normalized_word_vectors.T)
    return cs_matrix


def most_similar_words(word_id: int, cs_matrix: np.ndarray, top_n: int = 5):
    """コサイン類似度が最も高い単語の ID を取り出す"""
    similarities = cs_matrix[word_id, :]
    similar_word_ids = np.argsort(similarities)[::-1]
    # 基本的にコサイン類似度が最も高い ID は自分自身になるはずなので先頭は取り除く
    return similar_word_ids[1: top_n + 1]


def main():
    # Penn Treebank コーパスを読み込む
    corpus_sentences = load_corpus('ptb.train.txt')

    # コーパスを単語に分割する
    corpus_words = list(sentences_to_words(corpus_sentences))

    # 単語 -> ID
    word_to_id = word_id_mappings(corpus_words)

    # コーパスの語彙数
    vocab_size = len(word_to_id.keys())

    # コーパスの単語を ID に変換する
    corpus_ids = words_to_ids(corpus_words, word_to_id)

    # 共起行列を計算する
    co_matrix = co_occurence_matrix(corpus_ids, vocab_size, window_size=2)

    # PPMI を計算する
    ppmi_matrix = positive_pointwise_mutual_information(co_matrix)

    # 特異値分解 (Singular Value Decomposition; SVD) する
    # 特異値の大きな次元を基準に先頭を取り出すことで次元圧縮する
    word_vectors, _, _ = randomized_svd(ppmi_matrix, n_components=100)

    # 単語を表すベクトル間のコサイン類似度を計算する
    cs_matrix = cosine_similarity_matrix(word_vectors)

    # ID -> 単語
    id_to_word = {value: key for key, value in word_to_id.items()}

    # いくつか似ているベクトルを持った単語を確認してみる
    example_words = ['you', 'year', 'car', 'toyota']
    for target_word in example_words:
        # ID に変換した上で最も似ている単語とそのベクトルを取り出す
        print(f'The most similar words of "{target_word}"')
        target_word_id = word_to_id[target_word]
        most_similar_word_ids = most_similar_words(target_word_id, cs_matrix)
        # 単語とベクトルを表示する
        for rank, similar_word_id in enumerate(most_similar_word_ids, start=1):
            similar_word = id_to_word[similar_word_id]
            similarity = cs_matrix[target_word_id, similar_word_id]
            print(f'TOP {rank}: {similar_word} = {similarity}')
        print('-' * 50)


if __name__ == '__main__':
    main()

上記を適当な名前のモジュールとして保存した上で実行してみよう。 サンプルコードでは「you」「year」「car」「toyota」について類似の分散表現を持つ単語を調べている。

$ python pmisvd.py      
The most similar words of "you"
TOP 1: we = 0.7093088626861572
TOP 2: i = 0.699989378452301
TOP 3: yourself = 0.5935164093971252
TOP 4: anybody = 0.5696209073066711
TOP 5: ll = 0.537631094455719
--------------------------------------------------
The most similar words of "year"
TOP 1: month = 0.727214515209198
TOP 2: earlier = 0.6935610771179199
TOP 3: period = 0.6458954215049744
TOP 4: next = 0.6107259392738342
TOP 5: fiscal = 0.5819855332374573
--------------------------------------------------
The most similar words of "car"
TOP 1: auto = 0.706072986125946
TOP 2: luxury = 0.6855229735374451
TOP 3: vehicle = 0.48040351271629333
TOP 4: cars = 0.46684157848358154
TOP 5: truck = 0.4660819172859192
--------------------------------------------------
The most similar words of "toyota"
TOP 1: motor = 0.722280740737915
TOP 2: motors = 0.6682790517807007
TOP 3: nissan = 0.6666470170021057
TOP 4: honda = 0.6519786715507507
TOP 5: lexus = 0.6209456920623779
--------------------------------------------------

見ると、意味的に近い単語が類似した分散表現を持つように学習できていることがわかる。 「toyota」の類似語に「motor」と「motors」が別々に出てきてしまっているのはステミング (Stemming) の処理が入っていないため。

サンプルコードで共起行列を作るのに使ったウィンドウサイズは 2 ということを前提にして考えたとき、次のような感想を抱いた。

  • そんなに共起するか?っていう「you」と「we」や「i」が類似した分散表現になっているのは面白い
  • 「toyota」の類似語に「nissan」や「honda」があるところには納得感がある
    • 同じ文章の中で並べられることも多いだろうし

ところで、書籍の中には共起行列で特定の単語に対応する行ベクトルもカウントベースの分散表現の一種とあった。 個人的にその認識はなかったので、読んだときには「なるほど」と感じた。

ゼロから作るDeep Learning ❷ ―自然言語処理編

ゼロから作るDeep Learning ❷ ―自然言語処理編

  • 作者:斎藤 康毅
  • 発売日: 2018/07/21
  • メディア: 単行本(ソフトカバー)

Python: REPL に複数行をペーストしたときの挙動が変わって困った件について

表題のとおりなんだけど、最近 Python の REPL に複数行のコードをペーストしたときの挙動が以前と変わってしまい困っていた。 その Python というのは、具体的には Homebrew でインストールしたものや、Pyenv を使ってソースコードからビルドしたもの。

使っている環境は次のとおり。

$ sw_vers
ProductName:    Mac OS X
ProductVersion: 10.15.7
BuildVersion:   19H114
$ brew info readline | head -n 1
readline: stable 8.1 (bottled) [keg-only]
$ pyenv --version
pyenv 1.2.22

もくじ

TL;DR

結論から先に述べると、これは Python のビルドに使った GNU Readline のオプションが関係している。 具体的には、バージョン 8.1 から enable-bracketed-paste というオプションのデフォルト値が off から on に変更されたらしい。

そのため、問題のワークアラウンドとしては、このオプションの値を明示的に指定すれば良い。 たとえば、以下のようなコマンドで GNU Readline の設定ファイルを作ろう。

$ echo "set enable-bracketed-paste off" >> ~/.inputrc

これで、以前と同じ挙動になる。

問題についてもうちょっと詳しく

たとえば、次のような 2 行をコピーアンドペーストしたいときを想定する。

a = 1
b = 2

以前の REPL であれば、次のように別々の行として認識された。

>>> a = 1
>>> b = 2

それが、問題のある環境では以下のように SyntaxError 例外になってしまう。

>>> a = 1
b = 2
  File "<stdin>", line 1
    a = 1
b = 2
         ^
SyntaxError: multiple statements found while compiling a single statement

複数行を一度にペーストできないのは REPL として使い勝手が良くない。

上記の問題が起こる環境には、バージョンは問わず割と最近にビルドしたもの、という共通点があった。 そのため、GNU Readline が怪しいという当たりはついていたので、そこを重点的に調べていったところ以下のページにたどり着いた。

github.com

どうやら GNU Readline のバージョン 8.1 から、enable-bracketed-paste というオプションがデフォルトで on になったらしい。 その影響を受けて、複数行のペーストが上記のような挙動に変わってしまった。

最近の端末エミュレータには、Bracketed Paste Mode というものがある。 これは、通常の文字の入力とペーストによる入力を区別できるようにすることを意図して追加された機能らしい。 そして、GNU Readline はコマンドラインの編集操作に関する機能を提供するライブラリとなっている。 これまで、GNU Readline はペースト時のデフォルトの設定として Bracketed Paste Mode を無効にしていたようだ。 それが、8.1 からデフォルトの設定が変更になって、今回の問題が生じた、ということらしい。

それはそれとして、Python の REPL が Bracketed Paste に対応するという話もあると思う。 対応していると、空行の入ったスクリプトのコードをそのまま入れてもエラーにならないという嬉しさがあるはず。 ただ、調べたところ以下のチケットはあったが現状であまり話は前に進んでいないようだ。

bugs.python.org

いじょう。

Python: TensorFlow/Keras で Entity Embedding を試してみる

ニューラルネットワークでカテゴリ変数を扱う方法としては One-Hot エンコーディングがある。 しかし、One-Hot エンコーディングでは特徴量のカーディナリティが高いと扱う次元数が大きくなる。 そこで、今回紹介する Entity Embedding を使うと、ラベルエンコードしたカテゴリ変数をニューラルネットワークで扱うことができるようになる。 Entity Embedding では、Embedding 層を使うことでカテゴリ変数ごとにパラメータの重み (分散表現) を学習する。 今回は TensorFlow/Keras で Entity Embedding を試してみる。

使った環境は次のとおり。

$ sw_vers         
ProductName:    Mac OS X
ProductVersion: 10.15.7
BuildVersion:   19H114
$ python -V                                                 
Python 3.8.7

下準備

あらかじめ、使用するパッケージをインストールしておく。

$ pip install tensorflow scikit-learn sklearn-pandas seaborn

Embedding 層について

Entity Embedding を理解する上では、まず Embedding 層について理解する必要がある。 Keras では Embedding 層の実装として tensorflow.keras.layers.Embedding が使える。

keras.io

上記を実装するには tensorflow.nn.embedding_lookup() という関数が使われている。

www.tensorflow.org

そこで、まずはこの関数の振る舞いについて見ていくことにしよう。 はじめに Python のインタプリタを起動する。

$ python

TensorFlow をインポートしたら、例として次のような 5 行 2 列の Tensor オブジェクトを用意する。

>>> import tensorflow as tf
>>> tensor = tf.constant([
...     [1, 2],
...     [3, 4],
...     [5, 6],
...     [7, 8],
...     [9, 10],
... ])
>>> tensor
<tf.Tensor: shape=(5, 2), dtype=int32, numpy=
array([[ 1,  2],
       [ 3,  4],
       [ 5,  6],
       [ 7,  8],
       [ 9, 10]], dtype=int32)>

上記の Tensor オブジェクトに対してtf.nn.embedding_lookup() 関数を使ってみよう。 この関数を使ってできることは単純で、行のインデックス番号を指定すると、その行を取り出すことができる。 例えば、次のように 0, 2, 3 をリストで渡すと、その行に対応した要素を取り出すことができる。

>>> tf.nn.embedding_lookup(tensor, [0, 2, 3])
<tf.Tensor: shape=(3, 2), dtype=int32, numpy=
array([[1, 2],
       [5, 6],
       [7, 8]], dtype=int32)>

先ほど作った Tensor オブジェクトから 0, 2, 3 行目が取り出されていることがわかる。

できることがわかったところで、これの何が嬉しいのか考えてみよう。 先ほど作った Tensor オブジェクトで、行がカテゴリ変数の各カテゴリ、列がそのカテゴリの重み (分散表現) に対応すると捉えたらどうだろうか。 カテゴリ変数を扱う上で、One-Hot にしなくても、ラベルエンコードしたカテゴリ変数についてインデックス番号ごとに分散表現を管理できることがわかる。

念の為、tensorflow.keras.layers.Embedding の挙動についても確認しておこう。 先ほど作った Tensor オブジェクトと同じ重みを持った Embedding オブジェクトを次のように作る。

>>> initializer = tf.keras.initializers.Constant(
    value=[
        [1, 2],
        [3, 4],
        [5, 6],
        [7, 8],
        [9, 10],
    ]
)
>>> embedding_layer = tf.keras.layers.Embedding(5, 2, embeddings_initializer=initializer)

あとは入力として、先ほど tf.nn.embedding_lookup() 関数の引数に使ったのと同じインデックス番号を渡してみよう。

>>> embedding_layer(inputs=tf.constant([0, 2, 3]))
<tf.Tensor: shape=(3, 2), dtype=float32, numpy=
array([[1., 2.],
       [5., 6.],
       [7., 8.]], dtype=float32)>

すると、ちゃんとインデックス番号に対応した重みが出力として得られることがわかる。

カテゴリ変数を Entity Embedding で扱う MLP を書いてみる

続いては、実際にカテゴリ変数を Entity Embedding で扱うサンプルコードを見てみる。 以下では、Diamonds データセットを使って、3 層の MLP (Multi Layer Perceptron) で価格 (price) を推定する回帰問題を学習している。 Diamonds データセットには、カテゴリ変数として cutcolorclarity という特徴量がある。 今回は、それらのカテゴリ変数をラベルエンコードした数値として受け取って、2 次元の分散表現として学習させた。 モデルの評価は 3-Fold CV で、Out-of-Fold で推定した値を RMSLE で最後に出力している。

#!/usr/bin/env python3
# -*- coding: utf-8 -*-

import numpy as np
import tensorflow as tf
from sklearn.metrics import mean_squared_log_error
from sklearn_pandas import DataFrameMapper
from sklearn.model_selection import KFold
from sklearn.preprocessing import LabelEncoder
from sklearn.preprocessing import MinMaxScaler
import seaborn as sns
from matplotlib import pyplot as plt


class MultiLayerPerceptronWithEntityEmbedding(tf.keras.Model):
    """カテゴリ変数を Entity Embedding で扱う MLP"""

    def __init__(self):
        super().__init__()

        # cut を Entity Embedding する層
        self.cut_embedding = tf.keras.Sequential([
            tf.keras.layers.Input(shape=(1,)),
            tf.keras.layers.Embedding(5, 2),
            tf.keras.layers.Flatten(),
        ], name='cut_embedding')

        # color を Entity Embedding する層
        self.color_embedding = tf.keras.Sequential([
            tf.keras.layers.Input(shape=(1,)),
            tf.keras.layers.Embedding(7, 2),
            tf.keras.layers.Flatten(),
        ], name='color_embedding')

        # clarity を Entity Embedding する層
        self.clarity_embedding = tf.keras.Sequential([
            tf.keras.layers.Input(shape=(1,)),
            tf.keras.layers.Embedding(8, 2),
            tf.keras.layers.Flatten(),
        ], name='clarity_embedding')

        # MLP 層
        self.mlp = tf.keras.Sequential([
            tf.keras.layers.Dense(64,
                                  activation=tf.keras.activations.relu),
            tf.keras.layers.Dropout(0.25),
            tf.keras.layers.Dense(32,
                                  activation=tf.keras.activations.relu),
            tf.keras.layers.Dropout(0.25),
            tf.keras.layers.Dense(1),
        ], name='mlp')

    def call(self, inputs):
        # cut の Embedding
        cut = inputs[:, 0]
        cut_embed = self.cut_embedding(cut)

        # color の Embedding
        color = inputs[:, 1]
        color_embed = self.color_embedding(color)

        # clarity の Embedding
        clarity = inputs[:, 2]
        clarity_embed = self.clarity_embedding(clarity)

        # 残りの特徴量と連結する
        concat_layers = [
            cut_embed,
            color_embed,
            clarity_embed,
            inputs[:, 3:],
        ]
        x = tf.keras.layers.Concatenate()(concat_layers)

        # MLP 層に入れる
        x = self.mlp(x)

        return x


def main():
    # Diamonds データセットを読み込む
    df = sns.load_dataset('diamonds')
    x, y = df.drop(['price'], axis=1), df['price']

    mapper = DataFrameMapper([
        # カテゴリ変数はラベルエンコードする
        ('cut', LabelEncoder()),
        ('color', LabelEncoder()),
        ('clarity', LabelEncoder()),
        # 連続変数は Min-Max スケーリングで標準化する
        (['carat', 'depth', 'table', 'x', 'y', 'z'], MinMaxScaler()),
    ], df_out=True)

    # エンコードする
    x_transformed = mapper.fit_transform(x)
    print(x_transformed.head())

    models = []
    folds = KFold(n_splits=3, shuffle=True, random_state=42)
    for train_index, val_index in folds.split(x_transformed, y):
        train_x = x_transformed.iloc[train_index]
        train_y = y.iloc[train_index]
        valid_x = x_transformed.iloc[val_index]
        valid_y = y.iloc[val_index]

        # タスクを定義する
        model = MultiLayerPerceptronWithEntityEmbedding()
        optimizer = tf.keras.optimizers.Adam(learning_rate=1e-2)
        loss_function = tf.keras.losses.MeanSquaredLogarithmicError()
        model.compile(optimizer=optimizer,
                      loss=loss_function)
        model.build((None,) + train_x.shape[1:])

        # 学習に使うコールバック
        checkpoint_filepath = '/tmp/keras-checkpoint'
        callbacks = [
            # 検証データに対する損失が一定のエポック数の間に改善しないときは学習を打ち切る
            tf.keras.callbacks.EarlyStopping(monitor='val_loss',
                                             patience=50,
                                             verbose=1,
                                             mode='min'),
            # モデルを記録する
            tf.keras.callbacks.ModelCheckpoint(monitor='val_loss',
                                               filepath=checkpoint_filepath,
                                               save_best_only=True,
                                               verbose=1,
                                               mode='min'),
        ]

        # 学習する
        model.fit(train_x, train_y,
                  batch_size=1024,
                  epochs=1_000,
                  verbose=1,
                  validation_data=(valid_x, valid_y),
                  callbacks=callbacks,
                  )

        # 最良のモデルを読み込む
        model.load_weights(checkpoint_filepath)
        models.append(model)

    y_preds = np.zeros_like(y, dtype=np.float64)
    model_fold_mappings = zip(models, folds.split(x_transformed, y))
    for model, (_, val_index) in model_fold_mappings:
        # Out-of-Fold で予測を作る
        valid_x = x_transformed.iloc[val_index]
        y_pred = model.predict(valid_x)
        y_preds[val_index] = y_pred.ravel()

    rmsle = np.sqrt(mean_squared_log_error(y, y_preds))
    print(f'Root Mean Squared Log Error: {rmsle}')

    # predicted vs actual plot
    plt.scatter(y_preds, y)
    y_max = max(max(y_preds), max(y))
    plt.plot([0, y_max], [0, y_max], color='r')
    plt.xlabel('Predicted')
    plt.ylabel('Actual')
    plt.show()


if __name__ == '__main__':
    main()

上記を実行してみよう。 なお、データもネットワークも小さいので CPU で十分処理できる。

$ python entityembed.py
...
Root Mean Squared Log Error: 0.11266712415049261

回帰問題は評価指標だけだとイマイチイメージがつきにくいので真の価格と推定した価格の散布図も示す。

f:id:momijiame:20210115015343p:plain
真の価格と Out-of-Fold で推定した価格のプロット

とりあえず、カテゴリ変数を One-Hot しなくても学習できていることがわかった。 これができると、特徴量を扱う処理でもニューラルネットワーク用とそれ以外の差分が小さくなる余地ができてありがたい。

補足

今回使った Diamonds データセットについては、以下の理由からあえて Entity Embedding する必要はないかなという話もある。

  • カテゴリ変数のカーディナリティが小さいので One-Hot エンコーディングでも十分に扱える
  • カテゴリ変数が名義尺度ではなく順序尺度なので、順序にもとづいて Ordinal エンコーディングすることもできる

なお、軽く検証した限りでは、今回のデータとネットワークと評価指標だと性能は One-Hot Encoding > Entity Embedding > Ordinal Encoding という感じだった。 Ordinal Encoding がイマイチ効かないのは、順序尺度毎の寄与がすべて一定という仮定を置くことになって、それが誤りだからなのかな。

コード的な差分は、One-Hot エンコードに関しては前処理を次のようにする。

    mapper = DataFrameMapper([
        # カテゴリ変数は One-Hot エンコードする
        ('cut', LabelBinarizer()),
        ('color', LabelBinarizer()),
        ('clarity', LabelBinarizer()),
        (['carat', 'depth', 'table', 'x', 'y', 'z'], MinMaxScaler()),
    ], df_out=True)

Ordinal エンコードであれば、こう。

    mapper = DataFrameMapper([
        # カテゴリ変数を Ordinal エンコードした後に標準化する
        (['cut', 'color', 'clarity'], [
            OrdinalEncoder(categories=[
                ['Ideal', 'Premium', 'Very Good', 'Good', 'Fair'],
                ['D', 'E', 'F', 'G', 'H', 'I', 'J'],
                ['IF', 'VVS1', 'VVS2', 'VS1', 'VS2', 'SI1', 'SI2', 'I1'],
            ]),
            MinMaxScaler(),
        ]),
        (['carat', 'depth', 'table', 'x', 'y', 'z'], MinMaxScaler()),
    ], df_out=True)

そして、モデルは MLP 層だけにして上記の出力をそのまま突っ込んでやれば良い。

参考文献

元ネタの論文では、Entity Embedding のメリットとして扱う次元数の減少や学習の高速化以外についても触れられている。

arxiv.org

いじょう。

Python: LightGBM の cv() 関数と SHAP を使ってみる

以前、このブログでは機械学習モデルの解釈可能性を向上させる手法として SHAP を扱った。

blog.amedama.jp

上記のエントリでは、LightGBM の train() 関数と共に、モデルの学習に使ったデータを解釈していた。 今度は cv() 関数を使って、Out-of-Fold なデータを解釈する例を試してみる。 つまり、モデルにとって未知のデータを、どのような根拠で予測をしているのかざっくり把握することが目的になる。

使った環境は次のとおり。

$ sw_vers         
ProductName:    Mac OS X
ProductVersion: 10.15.7
BuildVersion:   19H114
$ python -V          
Python 3.8.6

もくじ

下準備

あらかじめ、必要なパッケージをインストールしておく。

$ pip install -U "lightgbm>3" shap scikit-learn pandas matplotlib

擬似的な二値分類用のデータを作って試してみる

前回のエントリと同じように、scikit-learn の API を使って擬似的な二値分類用のデータを作って試してみよう。 データは 100 次元あるものの、先頭の 5 次元しか分類には寄与する特徴になっていない。

サンプルコードを以下に示す。 基本的には cv() 関数から取り出した CVBooster を使って、Out-of-Fold な Prediction を作る要領で SHAP Value を計算するだけ。 あとは、それを好きなように可視化していく。 以下では、CVBooster を構成している各 Booster について summary_plot() で可視化した。

#!/usr/bin/env python3
# -*- coding: utf-8 -*-

import numpy as np
import pandas as pd
import shap
import lightgbm as lgb
from sklearn.datasets import make_classification
from sklearn.model_selection import StratifiedKFold
from matplotlib import pyplot as plt


def main():
    # 疑似的な教師信号を作るためのパラメータ
    args = {
        # データ点数
        'n_samples': 10_000,
        # 次元数
        'n_features': 100,
        # その中で意味のあるもの
        'n_informative': 5,
        # 重複や繰り返しはなし
        'n_redundant': 0,
        'n_repeated': 0,
        # タスクの難易度
        'class_sep': 0.65,
        # 二値分類問題
        'n_classes': 2,
        # 生成に用いる乱数
        'random_state': 42,
        # 特徴の順序をシャッフルしない (先頭の次元が informative になる)
        'shuffle': False,
    }
    # 擬似的な二値分類用の教師データを作る
    dummy_x, dummy_y = make_classification(**args)
    # 一般的なユースケースを想定して Pandas のデータフレームにしておく
    col_names = [f'feature_{i:02d}'
                 for i in range(dummy_x.shape[1])]
    df = pd.DataFrame(data=dummy_x, columns=col_names)
    df['target'] = pd.Series(data=dummy_y, dtype=bool)
    # 教師データ
    train_x, train_y = df.drop(['target'], axis=1), df.target

    # データの分割方法は Stratified 5-Fold CV
    folds = StratifiedKFold(n_splits=5,
                            shuffle=True,
                            random_state=42,
                            )
    # LightGBM のデータセット形式にする
    lgb_train = lgb.Dataset(train_x, train_y)
    # 学習時のパラメータ
    lgb_params = {
        'objective': 'binary',
        'metric': 'binary_logloss',
        'first_metric_only': True,
        'verbose': -1,
    }
    # 学習する
    cv_result = lgb.cv(params=lgb_params,
                       train_set=lgb_train,
                       folds=folds,
                       num_boost_round=10_000,
                       early_stopping_rounds=100,
                       verbose_eval=10,
                       return_cvbooster=True,
                       )
    # CVBooster を取り出す
    cvbooster = cv_result['cvbooster']

    # Out-of-Fold なデータで SHAP Value を計算する
    split_indices = list(folds.split(train_x, train_y))
    cv_shap_values = np.zeros_like(train_x, dtype=np.float)
    # Booster と学習に使ったデータの対応関係を取る
    booster_split_mappings = list(zip(cvbooster.boosters,
                                      split_indices))
    for booster, (_, val_index) in booster_split_mappings:
        # Booster が学習に使っていないデータ
        booster_val_x = train_x.iloc[val_index]
        # SHAP Value を計算する
        explainer = shap.TreeExplainer(booster,
                                       model_output='probability',
                                       data=booster_val_x)
        train_x_shap_values = explainer.shap_values(booster_val_x)
        cv_shap_values[val_index] = train_x_shap_values

    # 各教師データに対応する SHAP Value さえ計算できれば、後は好きに可視化すれば良い
    # 試しに各 Booster の summary_plot を眺めてみる
    plt.figure(figsize=(12, 16))
    for i, (_, val_index) in enumerate(split_indices):
        # 描画位置
        plt.subplot(3, 2, i + 1)
        # 各 Booster の Explainer が出力した SHAP Value ごとにプロットしていく
        shap.summary_plot(shap_values=cv_shap_values[val_index],
                          features=train_x.iloc[val_index],
                          feature_names=train_x.columns,
                          plot_size=None,
                          show=False)
        plt.title(f'Booster: {i}')
    plt.show()

    # 1 つのグラフにするなら、これだけで良い
    """
    shap.summary_plot(shap_values=cv_shap_values,
                      features=train_x,
                      feature_names=train_x.columns)
    """


if __name__ == '__main__':
    main()

上記を保存したら、実行してみよう。 パフォーマンス的には、SHAP Value の計算部分を並列化すると、もうちょっと速くできそう。

$ python lgbcvshap.py
[10]    cv_agg's binary_logloss: 0.417799 + 0.00797668
[20]    cv_agg's binary_logloss: 0.333266 + 0.0108777
[30]    cv_agg's binary_logloss: 0.30215 + 0.0116352
...(省略)...
[220]   cv_agg's binary_logloss: 0.282142 + 0.0185382
[230]   cv_agg's binary_logloss: 0.283128 + 0.0188667
[240]   cv_agg's binary_logloss: 0.284197 + 0.0195125
 98%|===================| 1951/2000 [00:29<00:00]

実行すると、次のようなプロットが得られる。

f:id:momijiame:20210108185439p:plain
各 Booster の SHAP Value を可視化したチャート

基本的には、どの Booster の結果も、先頭 5 次元の SHAP Value が大きくなっていることが見て取れる。

いじょう。

Ubuntu 20.04 LTS に後から GUI (X Window System) を追加する

Ubuntu 20.04 LTS のサーバ版をインストールした場合には、デフォルトでは GUI 環境が用意されない。 しかし、後から必要になる場合もある。 今回は、そんなときどうするかについて。 なお、必要な操作は Ubuntu 18.04 LTS の場合と変わらなかった。

使った環境は次のとおり。

$ lsb_release -a
No LSB modules are available.
Distributor ID: Ubuntu
Description:    Ubuntu 20.04.1 LTS
Release:    20.04
Codename:   focal
$ uname -r
5.4.0-59-generic

もくじ

下準備

あらかじめ、リポジトリの情報を更新しておく。

$ sudo apt-get update

デスクトップ環境が必要なとき

デスクトップ環境が必要なときは ubuntu-desktop パッケージをインストールする。

$ sudo apt-get -y install ubuntu-desktop

かなり色々と入るので気長に待つ必要がある。

インストールが終わったら再起動する。

$ sudo shutdown -r now

うまくいけば自動的にデスクトップ環境が有効な状態で起動してくる。

f:id:momijiame:20210106073254p:plain
Ubuntu 20.04 LTS のデスクトップ環境

X Window System だけで良いとき

デスクトップ環境は必要なく、単純に X Windows System だけで良いときは xserver-xorg パッケージをインストールする。 これは、たとえば X11 Forwarding を使って、リモートで起動したアプリケーションの画面をローカルに出したいときに多い。

$ sudo apt-get -y install xserver-xorg

X Windows System を使うアプリケーションとして xeyes などを起動して動作確認する。

$ sudo apt-get -y install x11-apps
$ xeyes

f:id:momijiame:20210106072626p:plain
xeyes

いじょう。

普段使ってるオーラルケア用品について

このブログでは、年に何回か技術系じゃないエントリも書いているんだけど、今回もそれ。 普段使っているオーラルケア用品で、これは良いなと思っているものを理由と一緒に書いていく。 なお、完全なる主観に過ぎないことをあらかじめ断っておきます。

マウスウォッシュ

はじめにマウスウォッシュから。 ジャンルとしては液体ハミガキ粉に分類されるのかな。 歯磨きをする前に口に含んで、ゆすいでから吐き出すやつ。 普段はリステリンの紫 (トータルケア) を使っている。

初めて使うときは以下の低刺激タイプから入るのが良さそう。

以下は口に含むと口の中がブワッてなるので最初は驚くかも。

効果としては、使うと口臭が抑えられる感じがある。

電動歯ブラシ

歯医者さんの検診の歯磨き指導で毎回磨き残しがあると言われ続けて、ついには磨く時間が 5 分を越えたときに「これ以上は手でやるのムリ」と判断して購入したのが電動歯ブラシ。 導入後の指導では「まあ良いでしょう」というコメントをもらえた。

今は、もう廃版になっているフィリップスのモデルを買ってから使い続けている。 他のメーカーは使ったことがないのでわからない。 ただ、選定する上では先端の交換用ブラシのアタッチメントの互換性とか、値段は考慮した方が良いかも。 ランニングコストに関わってくるので。 とりあえず、フィリップスに関しては 10 年くらいアタッチメントの互換性は保たれている。 交換用のブラシは一本あたり千円くらいはする。

フロス

歯ブラシだけだと歯の間の掃除はできない、みたいなことはよく聞くけど本当にそうだと思う。 丁寧に磨いた後でも、フロスを使うと口をゆすいだときに食べかすが出てくる。

リーチデンタルフロス ワックス 18M

リーチデンタルフロス ワックス 18M

  • メディア: ヘルスケア&ケア用品

あと、最初使い始めた頃はフロスが当たった歯肉から血が出てきて驚いた。 結局のところ、それは歯肉炎になっているのが原因だったのだと思う。 使い続けると血は出なくなるし、歯肉が引き締まる感じがある。 使ってない人には是非使ってもらいたい。

歯みがき粉

歯みがき粉はマウスウォッシュとは別に、目的に合わせて 2 種類を使い分けている。

ひとつは以下のビリリアントモアっていうやつで、これはホワイトニング効果の高いもの。 リモートワークが始まってコーヒーと紅茶の消費量が増えて、歯のステインに悩んでいたんだけど、これのおかげで解消した。 ステインが気になるところを、これを付けた電動歯ブラシで重点的に磨くと本当にキレイになる。 最初は連続して使っていたけど、今は週に一回くらいのペースで使っている。

もうひとつは知覚過敏を防ぐ効果のあるシュミテクトを使っている。 冷たいものを食べるとしみるのが気になって使い始めたんだけど、最近はそれなりにマシになっている気がしないでもない。 いや、もしかすると気のせいかも。 ブリリアントモアを使わない日はこちらを使っている。

キシリトールタブレット

これはちょっと番外編かもしれないけど、テーブルにキシリトールタブレットを置いといて、気が向いたときにポリポリと食べてる。 きっかけは子どもの歯みがきトレーニングでご褒美に使えるかなと考えて、大人が味見がてら買ってから続いている。 ほのかな甘さでなかなか美味しい。

ハキラ ブルーベリー 45粒

ハキラ ブルーベリー 45粒

  • メディア: ヘルスケア&ケア用品

子どもにも奥歯が生えたら、歯みがきのご褒美にあげようと思う。

まとめ

とりあえず今フロスを使っていないなら是非使ってもらいたい。 面倒くさがりで時短したいなら電動ハブラシはおすすめ。 口臭ケアが気になってる人はリステリン良いと思う。 歯磨き粉は用途に合わせて。

そんなかんじで。