; oi: 表現
ラベル 表現 の投稿を表示しています。 すべての投稿を表示
ラベル 表現 の投稿を表示しています。 すべての投稿を表示

2016年11月15日火曜日

話題のTensorFlow・LINEトーク履歴を用いて対話ボットを作ってみた(5)

ケーススタディ

前振りが長くなったが、開発したソースコードを用いて実際に「対話ボット」を学習し、実行した結果を示す。

学習データ

学習には以下の日常で頻繁に利用している家族とのLINEトークデータを用いた。
  • 全LINEトークデータ 17,796ペア(in/out)
  • trainデータ 16,017ペア(in/out)#全データの9割
  • devデータ 1,779ペア(in/out)#全データの1割

ディレクトリ(学習開始時)

学習開始時は、以下の3つのpythonファイルと2つのディレクトリを同階層に配置する。また、line_talk_dataディレクトリには、学習データとして作成した4種類のファイルを格納する。(学習後には中間生成物が各フォルダに生成される)

  • chatbot.py
  • data_utils.py
  • seq2seq_model.py
  • line_talk_data #学習データ格納用ディレクトリ
    • line_talk_train.out
    • line_talk_train.in
    • line_talk_dev.out
    • line_talk_dev.in
  • line_talk_train #学習結果のcheckpointデータ格納用ディレクトリ

学習の実行

python chatbot.py

実行後のコンソールを以下に示す。
chatbot$ python chatbot.py 
Preparing LINE talk data in line_talk_data
Creating vocabulary line_talk_data/vocab40000.out from data line_talk_data/line_talk_train.out
Creating vocabulary line_talk_data/vocab40000.in from data line_talk_data/line_talk_train.in
Tokenizing data in line_talk_data/line_talk_train.out
Tokenizing data in line_talk_data/line_talk_train.in
Tokenizing data in line_talk_data/line_talk_dev.out
Tokenizing data in line_talk_data/line_talk_dev.in
Creating 3 layers of 256 units.
Created model with fresh parameters.
Reading development and training data (limit: 0).
global step 100 learning rate 0.5000 step-time 0.66 perplexity 8820.34
  eval: bucket 0 perplexity 3683.12
  eval: bucket 1 perplexity 4728.98
  eval: bucket 2 perplexity 4118.81
  eval: bucket 3 perplexity 5504.88
以下のように、ある程度収束してきたところで、学習を切り上げる。 今回は以下のスペックのMac book pro にて8時間程度学習を行った。

CPU : 2.9 GHz Intel Core i5
メモリ: 16 GB 1867 MHz DDR3

この時、line_talk_trainディレクトリの中に、学習ステップに応じた .ckptファイルが蓄積されている。対話実行時に、蓄積された最新の.ckptファイルはリストアされる。
global step 17000 learning rate 0.3812 step-time 0.49 perplexity 30.66
  eval: bucket 0 perplexity 29.40
  eval: bucket 1 perplexity 45.61
  eval: bucket 2 perplexity 44.65
  eval: bucket 3 perplexity 85.85
global step 17100 learning rate 0.3812 step-time 0.51 perplexity 30.04
  eval: bucket 0 perplexity 113.59
  eval: bucket 1 perplexity 55.54
  eval: bucket 2 perplexity 39.45
  eval: bucket 3 perplexity 45.94

対話の実行(--decode)

python chatbot.py --decode

--decodeオプションをつけて実行することで、対話モードで実行できる。 以下に「対話ボット」の対話例を示す。 最新の.ckptファイルをリストアされ、対話モードに入っていることが確認出来る。
chatbot$ python chatbot.py --decode
Reading model parameters from line_talk_train/chatbot.ckpt-17600
> 

以下に対話例を示す。 ([Morpho]タグ行)は入力の形態素解析の結果を出力している。
> 今日は何時に帰る?
([Morpho]:今日 は 何 時 に 帰る ?)
00 : 00 くらい か なー !
> 今から帰りまーす
([Morpho]:今 から 帰り ま ー す)
今 から ます !
> おつかれー
([Morpho]:お つかれ ー)
[ スタンプ ]
> 退社しましたー!
([Morpho]:退社 し まし た ー !)
00 : 00 くらい か なー !
> お腹空いたのでゆっくり食べてます〜
([Morpho]:お腹 空い た ので ゆっくり 食べ て ます 〜)
たいしゃ

そこそこの対話ができていることがわかる。
「今日は何時に帰る?」という質問に対して、時刻を返答しようとしていることがわかる。パターンとしては上手く学習できているが、 実際に情報がないので正しい時刻を返せていないのは残念であるが。
「ゆっくり食べてます〜(先にご飯を食べています)」という情報に対して、遅れて「たいしゃ」したというのも、日常でよくあるやりとりを上手く学習できているといえる。
また、LINEトークのテキストデータのみを学習しているため、一部の返答が[スタンプ]になってしまっているのも仕方がない結果であろう。

まとめ

LINEトーク履歴のテキストデータを用いた「対話ボット」を開発してみた結果、個人のLINEトークの返し方を学習し、それっぽい回答をしてくれることがわかった。翻訳と違い、「良い」「悪い」の基準が極めて曖昧なため、評価が難しいのは事実であるが、可能性を感じる結果にはなった。

今後は、データ量を増加させ、時系列を加味して対話データの生成するなど、学習データの洗練を行いたい。また、今回ハイパーパラメータについても、計算量削減のため、デフォルトの設定よりもだいぶ小さい値を用いている。データ量の増加に合わせて、ハイパーパラメータの調整も併せて行うことで、より自然な対話ができるようになると思われる。

参考



話題のTensorFlow・LINEトーク履歴を用いて対話ボットを作ってみた(1)

話題のTensorFlow・LINEトーク履歴を用いて対話ボットを作ってみた(2)

話題のTensorFlow・LINEトーク履歴を用いて対話ボットを作ってみた(3)

話題のTensorFlow・LINEトーク履歴を用いて対話ボットを作ってみた(4)

話題のTensorFlow・LINEトーク履歴を用いて対話ボットを作ってみた(4)

2. 「対話ボット」学習ロジックの実装


参考2:Tensorflow: Sequence-to-Sequence Models に含まれる、models/rnn配下の以下の3つのソースコードを基に、「対話ボット」に必要な修正を加える。実行時のメインメソッドを含む translate.py をchatbot.pyとして修正を加えた。


No.srcdescription
1translate/seq2seq_model.pyNeural translation sequence-to-sequence model.
2translate/data_utils.pyHelper functions for preparing translation data.
3translate/translate.pyBinary that trains and runs the translation model.

学習ロジック関連で、プログラムに修正を加えた点は以下だけである。
2. data_utils.py に対して「(LINEトーク履歴から作成した)学習用データ」の読み込み部分にprepare_line_talk_dataメソッドを作成
3. chatbot.py に対してdata_utils.py内のprepare_line_talk_dataメソッドを呼び出すように修正

3. 「対話ボット」対話ロジックの実装

対話ロジック関連で、プログラムに修正を加えた点は以下だけである。
3. chatbot.py に対して、Decode時の日本語の形態素解析処理の追加

英仏翻訳のためのsequence-to-sequenceモデルに対して、これだけの修正を加えるだけで、「対話ボット」として利用可能となる。

最終的に、以下の3つのソースコードとdataディレクトリを同階層に配備するだけで動作する。

ソースコードを以下に示す。
[Source Code : data_utils.py]
# Copyright 2016 y-euda. All Rights Reserved.
# The following modifications are added based on tensorflow/models/rnn/translate/data_utils.py.
# - prepare_line_talk_data() is created to load LINE talk data text file.
#
#==============================================================================
# Copyright 2015 The TensorFlow Authors. All Rights Reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
#     http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
# ==============================================================================

"""Utilities for downloading data from WMT, tokenizing, vocabularies."""
from __future__ import absolute_import
from __future__ import division
from __future__ import print_function

import gzip
import os
import re

from tensorflow.python.platform import gfile
import tensorflow as tf

# Special vocabulary symbols - we always put them at the start.
_PAD = b"_PAD"
_GO = b"_GO"
_EOS = b"_EOS"
_UNK = b"_UNK"
_START_VOCAB = [_PAD, _GO, _EOS, _UNK]

PAD_ID = 0
GO_ID = 1
EOS_ID = 2
UNK_ID = 3

# Regular expressions used to tokenize.
_WORD_SPLIT = re.compile(b"([.,!?\"':;)(])")
_DIGIT_RE = re.compile(br"\d")

def gunzip_file(gz_path, new_path):
  """Unzips from gz_path into new_path."""
  print("Unpacking %s to %s" % (gz_path, new_path))
  with gzip.open(gz_path, "rb") as gz_file:
    with open(new_path, "wb") as new_file:
      for line in gz_file:
        new_file.write(line)

def basic_tokenizer(sentence):
  """Very basic tokenizer: split the sentence into a list of tokens."""
  words = []
  for space_separated_fragment in sentence.strip().split():
    words.extend(_WORD_SPLIT.split(space_separated_fragment))
  return [w for w in words if w]


def create_vocabulary(vocabulary_path, data_path, max_vocabulary_size,
                      tokenizer=None, normalize_digits=True):
  """Create vocabulary file (if it does not exist yet) from data file.

  Data file is assumed to contain one sentence per line. Each sentence is
  tokenized and digits are normalized (if normalize_digits is set).
  Vocabulary contains the most-frequent tokens up to max_vocabulary_size.
  We write it to vocabulary_path in a one-token-per-line format, so that later
  token in the first line gets id=0, second line gets id=1, and so on.

  Args:
    vocabulary_path: path where the vocabulary will be created.
    data_path: data file that will be used to create vocabulary.
    max_vocabulary_size: limit on the size of the created vocabulary.
    tokenizer: a function to use to tokenize each data sentence;
      if None, basic_tokenizer will be used.
    normalize_digits: Boolean; if true, all digits are replaced by 0s.
  """
  if not gfile.Exists(vocabulary_path):
    print("Creating vocabulary %s from data %s" % (vocabulary_path, data_path))
    vocab = {}
    with gfile.GFile(data_path, mode="rb") as f:
      counter = 0
      for line in f:
        counter += 1
        if counter % 100000 == 0:
          print("  processing line %d" % counter)
        line = tf.compat.as_bytes(line)
        tokens = tokenizer(line) if tokenizer else basic_tokenizer(line)
        for w in tokens:
          word = _DIGIT_RE.sub(b"0", w) if normalize_digits else w
          if word in vocab:
            vocab[word] += 1
          else:
            vocab[word] = 1
      vocab_list = _START_VOCAB + sorted(vocab, key=vocab.get, reverse=True)
      if len(vocab_list) > max_vocabulary_size:
        vocab_list = vocab_list[:max_vocabulary_size]
      with gfile.GFile(vocabulary_path, mode="wb") as vocab_file:
        for w in vocab_list:
          vocab_file.write(w + b"\n")

def initialize_vocabulary(vocabulary_path):
  """Initialize vocabulary from file.

  We assume the vocabulary is stored one-item-per-line, so a file:
    dog
    cat
  will result in a vocabulary {"dog": 0, "cat": 1}, and this function will
  also return the reversed-vocabulary ["dog", "cat"].

  Args:
    vocabulary_path: path to the file containing the vocabulary.

  Returns:
    a pair: the vocabulary (a dictionary mapping string to integers), and
    the reversed vocabulary (a list, which reverses the vocabulary mapping).

  Raises:
    ValueError: if the provided vocabulary_path does not exist.
  """
  if gfile.Exists(vocabulary_path):
    rev_vocab = []
    with gfile.GFile(vocabulary_path, mode="rb") as f:
      rev_vocab.extend(f.readlines())
    rev_vocab = [line.strip() for line in rev_vocab]
    vocab = dict([(x, y) for (y, x) in enumerate(rev_vocab)])
    return vocab, rev_vocab
  else:
    raise ValueError("Vocabulary file %s not found.", vocabulary_path)


def sentence_to_token_ids(sentence, vocabulary,
                          tokenizer=None, normalize_digits=True):
  """Convert a string to list of integers representing token-ids.

  For example, a sentence "I have a dog" may become tokenized into
  ["I", "have", "a", "dog"] and with vocabulary {"I": 1, "have": 2,
  "a": 4, "dog": 7"} this function will return [1, 2, 4, 7].

  Args:
    sentence: the sentence in bytes format to convert to token-ids.
    vocabulary: a dictionary mapping tokens to integers.
    tokenizer: a function to use to tokenize each sentence;
      if None, basic_tokenizer will be used.
    normalize_digits: Boolean; if true, all digits are replaced by 0s.

  Returns:
    a list of integers, the token-ids for the sentence.
  """

  if tokenizer:
    words = tokenizer(sentence)
  else:
    words = basic_tokenizer(sentence)
  if not normalize_digits:
    return [vocabulary.get(w, UNK_ID) for w in words]
  # Normalize digits by 0 before looking words up in the vocabulary.
  return [vocabulary.get(_DIGIT_RE.sub(b"0", w), UNK_ID) for w in words]


def data_to_token_ids(data_path, target_path, vocabulary_path,
                      tokenizer=None, normalize_digits=True):
  """Tokenize data file and turn into token-ids using given vocabulary file.

  This function loads data line-by-line from data_path, calls the above
  sentence_to_token_ids, and saves the result to target_path. See comment
  for sentence_to_token_ids on the details of token-ids format.

  Args:
    data_path: path to the data file in one-sentence-per-line format.
    target_path: path where the file with token-ids will be created.
    vocabulary_path: path to the vocabulary file.
    tokenizer: a function to use to tokenize each sentence;
      if None, basic_tokenizer will be used.
    normalize_digits: Boolean; if true, all digits are replaced by 0s.
  """
  if not gfile.Exists(target_path):
    print("Tokenizing data in %s" % data_path)
    vocab, _ = initialize_vocabulary(vocabulary_path)
    with gfile.GFile(data_path, mode="rb") as data_file:
      with gfile.GFile(target_path, mode="w") as tokens_file:
        counter = 0
        for line in data_file:
          counter += 1
          if counter % 100000 == 0:
            print("  tokenizing line %d" % counter)
          token_ids = sentence_to_token_ids(line, vocab, tokenizer,
                                            normalize_digits)
          tokens_file.write(" ".join([str(tok) for tok in token_ids]) + "\n")

def prepare_line_talk_data(data_dir, in_vocabulary_size, out_vocabulary_size, tokenizer=None):
  """Get line talk data into data_dir, create vocabularies and tokenize data.

  Args:
    data_dir: directory in which the data sets will be stored.
    in_vocabulary_size: size of the Input vocabulary to create and use.
    out_vocabulary_size: size of the Output vocabulary to create and use.
    tokenizer: a function to use to tokenize each data sentence;
      if None, basic_tokenizer will be used.

  Returns:
    A tuple of 6 elements:
      (1) path to the token-ids for Input training data-set,
      (2) path to the token-ids for Output training data-set,
      (3) path to the token-ids for Input development data-set,
      (4) path to the token-ids for Output development data-set,
      (5) path to the Input vocabulary file,
      (6) path to the Output vocabulary file.
  """
  # Get line_talk data to the specified directory.
  train_path = os.path.join(data_dir, "line_talk_train")               
  dev_path = os.path.join(data_dir, "line_talk_dev")                     
  
  # Create vocabularies of the appropriate sizes.
  out_vocab_path = os.path.join(data_dir, "vocab%d.out" % out_vocabulary_size ) 
  in_vocab_path = os.path.join(data_dir, "vocab%d.in"  % in_vocabulary_size ) 
  create_vocabulary(out_vocab_path, train_path + ".out", out_vocabulary_size, tokenizer)
  create_vocabulary(in_vocab_path, train_path + ".in", in_vocabulary_size, tokenizer)

  # Create token ids for the training data.
  out_train_ids_path = train_path + (".ids%d.out" % out_vocabulary_size)
  in_train_ids_path = train_path + (".ids%d.in" % in_vocabulary_size)
  data_to_token_ids(train_path + ".out", out_train_ids_path, out_vocab_path, tokenizer)
  data_to_token_ids(train_path + ".in", in_train_ids_path, in_vocab_path, tokenizer)

  # Create token ids for the development data.
  out_dev_ids_path = dev_path + (".ids%d.out" % out_vocabulary_size)
  in_dev_ids_path = dev_path + (".ids%d.in" % in_vocabulary_size)
  data_to_token_ids(dev_path + ".out", out_dev_ids_path, out_vocab_path, tokenizer)
  data_to_token_ids(dev_path + ".in", in_dev_ids_path, in_vocab_path, tokenizer)

  return (in_train_ids_path, out_train_ids_path,
          in_dev_ids_path, out_dev_ids_path,
          in_vocab_path, out_vocab_path)

[Source Code : chatbot.py]
# Copyright 2016 y-euda. All Rights Reserved.
# The following modifications are added based on tensorflow/models/rnn/translate/translate.py.
# - train() is modified to load LINE talk data text file.
# - decode() is modifed for the input sentence in Japanese.
#
# ==============================================================================
# Copyright 2015 The TensorFlow Authors. All Rights Reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
#     http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
# ==============================================================================

"""Binary for training seq2seq models and decoding from them.

Running this program without --decode will download the line talk corpus into
the directory specified as --data_dir and tokenize it in a very basic way,
and then start training a model saving checkpoints to --train_dir.

Running with --decode starts an interactive loop so you can see how
the current checkpoint translates English sentences into French.

See the following papers for more information on neural translation models.
 * http://arxiv.org/abs/1409.3215
 * http://arxiv.org/abs/1409.0473
 * http://arxiv.org/abs/1412.2007
"""
from __future__ import absolute_import
from __future__ import division
from __future__ import print_function

import math
import os
import random
import sys
import time
import logging

import numpy as np
import tensorflow as tf


import data_utils as data_utils
import seq2seq_model as seq2seq_model

from janome.tokenizer import Tokenizer

tf.app.flags.DEFINE_float("learning_rate", 0.5, "Learning rate.")
tf.app.flags.DEFINE_float("learning_rate_decay_factor", 0.99,
                          "Learning rate decays by this much.")
tf.app.flags.DEFINE_float("max_gradient_norm", 5.0,
                          "Clip gradients to this norm.")
tf.app.flags.DEFINE_integer("batch_size", 4, #64
                            "Batch size to use during training.")
tf.app.flags.DEFINE_integer("size", 256, "Size of each model layer.") #1024
tf.app.flags.DEFINE_integer("num_layers", 3, "Number of layers in the model.") #3
tf.app.flags.DEFINE_integer("en_vocab_size", 40000, "English vocabulary size.") #40000
tf.app.flags.DEFINE_integer("fr_vocab_size", 40000, "French vocabulary size.") #40000
tf.app.flags.DEFINE_string("data_dir", "line_talk_data", "Data directory")#data
tf.app.flags.DEFINE_string("train_dir", "line_talk_train", "Training directory.")#train
tf.app.flags.DEFINE_integer("max_train_data_size", 0,
                            "Limit on the size of training data (0: no limit).")
tf.app.flags.DEFINE_integer("steps_per_checkpoint", 100,#200
                            "How many training steps to do per checkpoint.")
tf.app.flags.DEFINE_boolean("decode", False,
                            "Set to True for interactive decoding.")
tf.app.flags.DEFINE_boolean("self_test", False,
                            "Run a self-test if this is set to True.")
tf.app.flags.DEFINE_boolean("use_fp16", False,
                            "Train using fp16 instead of fp32.")

FLAGS = tf.app.flags.FLAGS

# We use a number of buckets and pad to the closest one for efficiency.
# See seq2seq_model.Seq2SeqModel for details of how they work.
_buckets = [(5, 10), (10, 15), (20, 25), (40, 50)]


def read_data(source_path, target_path, max_size=None):
  """Read data from source and target files and put into buckets.

  Args:
    source_path: path to the files with token-ids for the source language.
    target_path: path to the file with token-ids for the target language;
      it must be aligned with the source file: n-th line contains the desired
      output for n-th line from the source_path.
    max_size: maximum number of lines to read, all other will be ignored;
      if 0 or None, data files will be read completely (no limit).

  Returns:
    data_set: a list of length len(_buckets); data_set[n] contains a list of
      (source, target) pairs read from the provided data files that fit
      into the n-th bucket, i.e., such that len(source) < _buckets[n][0] and
      len(target) < _buckets[n][1]; source and target are lists of token-ids.
  """
  data_set = [[] for _ in _buckets]
  with tf.gfile.GFile(source_path, mode="r") as source_file:
    with tf.gfile.GFile(target_path, mode="r") as target_file:
      source, target = source_file.readline(), target_file.readline()
      counter = 0
      while source and target and (not max_size or counter < max_size):
        counter += 1
        if counter % 100000 == 0:
          print("  reading data line %d" % counter)
          sys.stdout.flush()
        source_ids = [int(x) for x in source.split()]
        target_ids = [int(x) for x in target.split()]
        target_ids.append(data_utils.EOS_ID)
        for bucket_id, (source_size, target_size) in enumerate(_buckets):
          if len(source_ids) < source_size and len(target_ids) < target_size:
            data_set[bucket_id].append([source_ids, target_ids])
            break
        source, target = source_file.readline(), target_file.readline()
  return data_set


def create_model(session, forward_only):
  """Create chat model and initialize or load parameters in session."""
  dtype = tf.float16 if FLAGS.use_fp16 else tf.float32
  model = seq2seq_model.Seq2SeqModel(
      FLAGS.en_vocab_size,
      FLAGS.fr_vocab_size,
      _buckets,
      FLAGS.size,
      FLAGS.num_layers,
      FLAGS.max_gradient_norm,
      FLAGS.batch_size,
      FLAGS.learning_rate,
      FLAGS.learning_rate_decay_factor,
      forward_only=forward_only,
      dtype=dtype)
  ckpt = tf.train.get_checkpoint_state(FLAGS.train_dir)
  if ckpt and tf.gfile.Exists(ckpt.model_checkpoint_path):
    print("Reading model parameters from %s" % ckpt.model_checkpoint_path)
    model.saver.restore(session, ckpt.model_checkpoint_path)
  else:
    print("Created model with fresh parameters.")
    session.run(tf.initialize_all_variables())
  return model

def train():
  """Train a in->out chat model using LINE talk data."""
  # Prepare line talk data.
  print("Preparing LINE talk data in %s" % FLAGS.data_dir)
  in_train, out_train, in_dev, out_dev, _, _ = data_utils.prepare_line_talk_data(
      FLAGS.data_dir, FLAGS.en_vocab_size, FLAGS.fr_vocab_size)

  with tf.Session() as sess:
    # Create model.
    print("Creating %d layers of %d units." % (FLAGS.num_layers, FLAGS.size))
    model = create_model(sess, False)

    # Read data into buckets and compute their sizes.
    print ("Reading development and training data (limit: %d)."
           % FLAGS.max_train_data_size)
    dev_set = read_data(in_dev, out_dev)
    train_set = read_data(in_train, out_train, FLAGS.max_train_data_size)
    train_bucket_sizes = [len(train_set[b]) for b in xrange(len(_buckets))]
    train_total_size = float(sum(train_bucket_sizes))

    # A bucket scale is a list of increasing numbers from 0 to 1 that we'll use
    # to select a bucket. Length of [scale[i], scale[i+1]] is proportional to
    # the size if i-th training bucket, as used later.
    train_buckets_scale = [sum(train_bucket_sizes[:i + 1]) / train_total_size
                           for i in xrange(len(train_bucket_sizes))]

    # This is the training loop.
    step_time, loss = 0.0, 0.0
    current_step = 0
    previous_losses = []
    while True:
      # Choose a bucket according to data distribution. We pick a random number
      # in [0, 1] and use the corresponding interval in train_buckets_scale.
      random_number_01 = np.random.random_sample()
      bucket_id = min([i for i in xrange(len(train_buckets_scale))
                       if train_buckets_scale[i] > random_number_01])

      # Get a batch and make a step.
      start_time = time.time()
      encoder_inputs, decoder_inputs, target_weights = model.get_batch(
          train_set, bucket_id)
      _, step_loss, _ = model.step(sess, encoder_inputs, decoder_inputs,
                                   target_weights, bucket_id, False)
      step_time += (time.time() - start_time) / FLAGS.steps_per_checkpoint
      loss += step_loss / FLAGS.steps_per_checkpoint
      current_step += 1

      # Once in a while, we save checkpoint, print statistics, and run evals.
      if current_step % FLAGS.steps_per_checkpoint == 0:
        # Print statistics for the previous epoch.
        perplexity = math.exp(float(loss)) if loss < 300 else float("inf")
        print ("global step %d learning rate %.4f step-time %.2f perplexity "
               "%.2f" % (model.global_step.eval(), model.learning_rate.eval(),
                         step_time, perplexity))
        # Decrease learning rate if no improvement was seen over last 3 times.
        if len(previous_losses) > 2 and loss > max(previous_losses[-3:]):
          sess.run(model.learning_rate_decay_op)
        previous_losses.append(loss)
        # Save checkpoint and zero timer and loss.
        checkpoint_path = os.path.join(FLAGS.train_dir, "chatbot.ckpt")
        model.saver.save(sess, checkpoint_path, global_step=model.global_step)
        step_time, loss = 0.0, 0.0
        # Run evals on development set and print their perplexity.
        for bucket_id in xrange(len(_buckets)):
          if len(dev_set[bucket_id]) == 0:
            print("  eval: empty bucket %d" % (bucket_id))
            continue
          encoder_inputs, decoder_inputs, target_weights = model.get_batch(
              dev_set, bucket_id)
          _, eval_loss, _ = model.step(sess, encoder_inputs, decoder_inputs,
                                       target_weights, bucket_id, True)
          eval_ppx = math.exp(float(eval_loss)) if eval_loss < 300 else float(
              "inf")
          print("  eval: bucket %d perplexity %.2f" % (bucket_id, eval_ppx))
        sys.stdout.flush()

#--decode --data_dir line_talk_data --train_dir line_talk_data
def decode():
  with tf.Session() as sess:
    # Create model and load parameters.
    model = create_model(sess, True)
    model.batch_size = 1  # We decode one sentence at a time.

    # Load vocabularies.
    en_vocab_path = os.path.join(FLAGS.data_dir,
                                 "vocab%d.in" % FLAGS.en_vocab_size)
    fr_vocab_path = os.path.join(FLAGS.data_dir,
                                 "vocab%d.out" % FLAGS.fr_vocab_size)
    en_vocab, _ = data_utils.initialize_vocabulary(en_vocab_path)
    _, rev_fr_vocab = data_utils.initialize_vocabulary(fr_vocab_path)

    # Decode from standard input.
    sys.stdout.write("> ")
    sys.stdout.flush()
    sentence = sys.stdin.readline()
    t = Tokenizer()
    tokens = t.tokenize(sentence.decode('utf-8')) 
    sentence = ' '.join([token.surface for token in tokens]).encode('utf-8') 
    print('([Morpho]:'+ sentence +')')

    while sentence:
      # Get token-ids for the input sentence.
      token_ids = data_utils.sentence_to_token_ids(tf.compat.as_bytes(sentence), en_vocab)
      # Which bucket does it belong to?
      bucket_id = len(_buckets) - 1
      for i, bucket in enumerate(_buckets):
        if bucket[0] >= len(token_ids):
          bucket_id = i
          break
      else:
        logging.warning("Sentence truncated: %s", sentence) 

      # Get a 1-element batch to feed the sentence to the model.
      encoder_inputs, decoder_inputs, target_weights = model.get_batch(
          {bucket_id: [(token_ids, [])]}, bucket_id)
      # Get output logits for the sentence.
      _, _, output_logits = model.step(sess, encoder_inputs, decoder_inputs,
                                       target_weights, bucket_id, True)
      # This is a greedy decoder - outputs are just argmaxes of output_logits.
      outputs = [int(np.argmax(logit, axis=1)) for logit in output_logits]
      # If there is an EOS symbol in outputs, cut them at that point.
      if data_utils.EOS_ID in outputs:
        outputs = outputs[:outputs.index(data_utils.EOS_ID)]
      # Print out French sentence corresponding to outputs.
      print(" ".join([tf.compat.as_str(rev_fr_vocab[output]) for output in outputs]))
      print("> ", end="")
      sys.stdout.flush()
      sentence = sys.stdin.readline()
      t = Tokenizer()
      tokens = t.tokenize(sentence.decode('utf-8')) 
      sentence = ' '.join([token.surface for token in tokens]).encode('utf-8') 
      print('([Morpho]:'+ sentence +')')


def self_test():
  """Test the translation model."""
  with tf.Session() as sess:
    print("Self-test for neural translation model.")
    # Create model with vocabularies of 10, 2 small buckets, 2 layers of 32.
    model = seq2seq_model.Seq2SeqModel(10, 10, [(3, 3), (6, 6)], 32, 2,
                                       5.0, 32, 0.3, 0.99, num_samples=8)
    sess.run(tf.initialize_all_variables())

    # Fake data set for both the (3, 3) and (6, 6) bucket.
    data_set = ([([1, 1], [2, 2]), ([3, 3], [4]), ([5], [6])],
                [([1, 1, 1, 1, 1], [2, 2, 2, 2, 2]), ([3, 3, 3], [5, 6])])
    for _ in xrange(5):  # Train the fake model for 5 steps.
      bucket_id = random.choice([0, 1])
      encoder_inputs, decoder_inputs, target_weights = model.get_batch(
          data_set, bucket_id)
      model.step(sess, encoder_inputs, decoder_inputs, target_weights,
                 bucket_id, False)

def main(_):
  if FLAGS.self_test:
    self_test()
  elif FLAGS.decode:
    decode()
  else:
    train()

if __name__ == "__main__":
  tf.app.run()


話題のTensorFlow・LINEトーク履歴を用いて対話ボットを作ってみた(1)


話題のTensorFlow・LINEトーク履歴を用いて対話ボットを作ってみた(2)

話題のTensorFlow・LINEトーク履歴を用いて対話ボットを作ってみた(3)

話題のTensorFlow・LINEトーク履歴を用いて対話ボットを作ってみた(5)

話題のTensorFlow・LINEトーク履歴を用いて対話ボットを作ってみた(3)

開発のために必要なこと

テキストベースの「対話ボット」の開発にあたって、最近劇的な精度向上によって話題になっているGoogle翻訳のベースの技術として利用されている、Googleが開発したライブラリである「Tensorflow」の”encoder-decoder sequence-to-sequence model”を用いる。Tensorflowの利用にあたっては、C++とPythonのAPIが用意されているが、現時点(2016/11/14)では、Python APIが最も開発が進んでおり、手軽なため、以後Pythonで開発する事を前提とする。

ちなみにTensorflowは、 Apache 2.0 open source license の下で、OSSとして公開されており、本モデル以外にも、チュートリアルと共に様々なDNN応用例が実装されている。

参考1:「Google翻訳が進化!? 精度が向上したと話題に(ディープラーニングによる新翻訳システムが導入されたとみられています。)2016年11月12日 10時23分 更新」
http://nlab.itmedia.co.jp/nl/articles/1611/12/news021.html

Fig. Google翻訳にるArtificial Intelligenceの日本語訳結果

参考2:Tensorflow: Sequence-to-Sequence Models
https://www.tensorflow.org/versions/r0.11/tutorials/seq2seq/index.html#sequence-to-sequence-models

参考2:Tensorflow: Sequence-to-Sequence Models のチュートリアルでは、同モデルを用いて英仏翻訳を行う例が紹介されている。英語の単語のシーケンスを入力とし、フランス語の単語のシーケンスを出力するモデルである。この構造を「対話ボット」に適用する場合、以下の作業が必要となる。

1. 「対話ボット」学習データの整備
2. 「対話ボット」学習ロジックの実装
3. 「対話ボット」対話ロジックの実装

次に各作業の詳細について説明する。

1.「対話ボット」学習データの整備

チュートリアルの英仏翻訳学習用のデータは、対訳データと呼ばれるデータである。これは、英語の文章に対して、翻訳結果となるフランス語の文章が対となった形で、それぞれのファイルに格納され、各行が対をなしているデータである。また、英語もフランス語も各単語が半角スペースで区切られ、単語のシーケンスとなっている。

これに対して、今回対象とする対話ボットは、LINEトーク履歴データ(日本語)を用いた「対話ボット」であるが故に以下が必要となる。

・対話を形成する日本語文対の作成

LINE のトークデータは以下の手順で簡単にテキストデータとしてエクスポート可能である。

 (iPhoneの場合)出力対象のトーク画面→「設定」→「トーク履歴を送信」

エクスポートされたテキストデータは、以下のような形式である。

 ファイル名 :[LINE]トーク相手名.txt

ex1.[LINE]田中太郎.txt

 ファイル内容:HH:MM¥tトーク者¥t発話内容

ex2.
08:40 太郎 おはようー
08:40 太郎 ございます!
08:41 太郎 [スタンプ]
09:00 花子 今日は起きれたんだね。
09:02 太郎 [スタンプ]
 :  :  :

このファイルを用いて入力文と出力文のペアを作成する。

LINEトークの特徴として、一方的に短文を何度も発話する場合が往々にしてあるため、シーケンスの単位として、連続して発話した一連の内容を1単位とした。簡単のため、発話時刻の間隔は考慮せず、連続で発話したすべてをまとめて一つの単位とした。

ex3.
太郎 おはようーございます![スタンプ]
花子 今日は起きれたんだね。
太郎 [スタンプ]
 : :

また、発話の順序性については、本来は発話時刻を考慮してセッションを考え、対話となっているペア生成する必要がある。しかし、今回は上の処理で一連の発話内容を1単位とした後、最初の発話者を入力担当、次の発話者を出力担当と割り振り、順々にペアを生成した。

ex4. 入力部分
おはようーございます![スタンプ]
[スタンプ]
 : 

ex5. 出力部分
今日は起きれたんだね。
 : 

・日本語文の分かち書き

日本語や中国語のような文章中に区切り文字が存在しない文章は、明示的なシーケンスを表現するために、形態素解析を行い、意味のある単位(=形態素)で文を分解する必要がある。

今回はPure Pythonで開発された形態素解析エンジンであるJanome(蛇の目)を用いる。

Janome (0.2.8)
http://mocobeta.github.io/janome/

以下にサンプルコードを示す。

from janome.tokenizer import Tokenizer
    with open('../data/line_talk.in', mode = 'w') as fw:
        t = Tokenizer()
        for line in inputs:
            tokens = t.tokenize(line) 
            line = ' '.join([token.surface for token in tokens]).encode('utf-8') + '\n'            
            fw.write(line)

分かち書きを行った入力部分、出力部分をそれぞれファイルに格納する。

ex6. line_talk.in
おはようーございます ! [スタンプ]
[スタンプ]
 : 

ex7. line_talk.out
今日 は 起き れ た ん だ ね 。
 : 

話題のTensorFlow・LINEトーク履歴を用いて対話ボットを作ってみた(1)

話題のTensorFlow・LINEトーク履歴を用いて対話ボットを作ってみた(2)

話題のTensorFlow・LINEトーク履歴を用いて対話ボットを作ってみた(4)

話題のTensorFlow・LINEトーク履歴を用いて対話ボットを作ってみた(5)

話題のTensorFlow・LINEトーク履歴を用いて対話ボットを作ってみた(2)

どのような「対話ボット」を作るか?

「いわゆるAI」がみたすべき重要な要件としては、知性や人格を感じることにある。画像認識の分野における一般物体認識の圧倒的な技術革新は、とても役に立つ高性能な機械という印象を脱しえない。今回は、「何の役にも立たなくても良いが、人格を感じるにはどうするか?」にフォーカスしてみようと思う。

そのように考えていった時に、現時点で最も簡単にコンピュータが自然に人格を表現する方法は文字列によるコミュニケーションであろう。対話の仕方はいくつかあるが、対話時の表情や発された言葉の音声などの複合的な情報を用いた対話の方がより人間らしさを感じることは言うまでもないが、人工知能で再現することはそのメディアの分だけコストがかかる。

よって今回は、最もシンプルに内容だけで人格を感じさせられる可能性がある対話ボットであるテキストによる対話ボットを考える。つまるところ、これは以下のチューリングテストで想定している「機械」そのものである。

人間の判定者が、一人の(別の)人間と一機の機械に対して通常の言語での会話を行う。このとき人間も機械も人間らしく見えるように対応するのである。これらの参加者はそれぞれ隔離されている。判定者は、機械の言葉を音声に変換する能力に左右されることなく、その知性を判定するために、会話はたとえばキーボードとディスプレイのみといった、文字のみでの交信に制限しておく。判定者が、機械と人間との確実な区別ができなかった場合、この機械はテストに合格したことになる。


(出典:Wikipedia https://ja.m.wikipedia.org/wiki/チューリング・テスト )

また、対象とする対話は、人格を感じさせられることを狙っているため、複数人の大量の対話履歴データを用いて学習させるよりも、ある特定の個人の対話履歴を用いて学習させることが望ましい、と考えた。

そこで、最も頻繁に利用しているコミュニケーションツールであるLINEの履歴データの活用を考えた。私のLINEトーク履歴データを用いて「対話ボット」を学習させ、LINE上で行なわれているコミュニケーションを再現できれば、私という人格をある程度再現できたことに相当する。

今回のやるべきことを整理すると「LINEのトーク履歴データを学習し、ある個人の人格を感じさせられるテキストベースの『対話ボット』を開発すること」である。

話題のTensorFlow・LINEトーク履歴を用いて対話ボットを作ってみた(1)

話題のTensorFlow・LINEトーク履歴を用いて対話ボットを作ってみた(3)

話題のTensorFlow・LINEトーク履歴を用いて対話ボットを作ってみた(4)

話題のTensorFlow・LINEトーク履歴を用いて対話ボットを作ってみた(5)

話題のTensorFlow・LINEトーク履歴を用いて対話ボットを作ってみた(1)

「人工知能といえば」



最近のAIの隆盛はすさまじい。アカデミアにおける研究活動は勿論のこと、政府、企業におけるAI関連組織の編成や、AI関連予算のニュースも数多く取り沙汰されている。

一応、機械学習をかじったことがある身としては、AIと持て囃されている技術の大部分が、データから何かを統計的に学習するという点で従来の機械学習となんら変わりはなく見えてしまい、真新しさはほとんど感じない。

唯一の大きな違いとしては、計算機パワーの増大や、多種多様な学習データの増加によって、ディープラーニングという技術が、画像認識などの特定領域において従来技術に圧勝し、脚光を浴びたことであり、これこそがブームの正体だと思っていた。

しかしながら、先日友人と何気なく人工知能の会話になった際に「AIといったら、SiriやWatsonみたいなやつでは?」と言われてしまった。

文理を問わず、機械学習や最適化について、これまで学んだことの無い多くの人からしてみれば、AIの指し示す領域は、やはり「知性や人格をそこに感じる存在」なのである。SF映画の影響を色濃く受けていると考えられるが、人間の代替であり、延いては感情を兼ね備えて友人や敵になりうる存在としても想起されるようだ。そのAIの中で利用されている要素技術が、単なる統計だろうと、機械学習だろうと、ディープラーニングだろうと全く関係ないのである。

このような世間が考える「いわゆるAI」と「技術領域としてのAI」に乖離を感じたため、今回は「いわゆるAI」に主眼をおいて機械学習を活用してみようと思うに至った。

そこで、題材として選んだのが「対話ボット」である。

話題のTensorFlow・LINEトーク履歴を用いて対話ボットを作ってみた(2)

話題のTensorFlow・LINEトーク履歴を用いて対話ボットを作ってみた(3)

話題のTensorFlow・LINEトーク履歴を用いて対話ボットを作ってみた(4)

話題のTensorFlow・LINEトーク履歴を用いて対話ボットを作ってみた(5)


2016年6月12日日曜日

今更聞けない「モデリング」の重要性:モデリングをはじめて勉強する人におすすめの入門書4点

モデルを基にした思考方法は、社会人に必須のスキルの一つである。プレゼンテーションにおける表現や、多様なバックグラウンドを持つ他者とのコミュニケーションにも、モデリングの考え方の一つである、抽象化・単純化の思考は必須である。

今回は、近年ますます注目を浴びているモデリングの方法やモデルベースの思考法を習得し、実ビジネスに応用するための入門書を紹介する。

モデリングの方法

スーパープログラマーに学ぶ 最強シンプル思考術




本書は、モデルリングの入門書として最適な書籍といえよう。本書で取り扱うモデルは、様々なモデルの中でも最もシンプルな「四角」と「線」だけで構成されるモデルを扱っている。このモデルの書き方はシンプルであるが故に非常に汎用性の高いモデリングの方法となる。本書でモデリングの基礎を押さえておけば、その他の様々なモデリング手法、記法(UML、SysML、BPMN、OWLなど)を習得するための準備になることは想像に難くない。また、非常に多くの卑近なモデル例、それらのモデルの良い点、悪い点、さらには悪いモデルの改善プロセス、モデルの現実的な活用例までが丁寧に説明されている。

特筆すべきモデリングの基本は、同じ対象であっても、目的や視座によって、出来上がるモデルが変わることである。これはモデルの良し悪しとは別の次元の話である。どの側面にから対象を観察するかで、見方は変化するが、それはどれも間違っていない。射影する方向が異なるだけである。

もの・こと分析で成功するシンプルな仕事の構想法

対象をモデル化・シンプル化するための一つの方法として、モノとコトに分けて考える手法がある。そちらも参照して欲しい。

「モノとコトから考える仕事の本質とは」

モデルの活用


基本的なモデリングに慣れた後は、モデルの活用に向けて以下の書籍を読むことをお勧めする。

アナロジー思考




抽象化し、アナロジー(類推)思考を行うことは、誰でも多かれ少なかれ経験があるだろう。本書にはそのアナロジー思考に焦点を当て、最大限有効活用することで、新しいアイデアを生むための方法論が書かれている。対象物を、ある側面から見て本質的な部分に絞り、抽象的に構造化することで、類似の構造を持つケースからアイデアを借りてくることが可能になる。自分が理解している別の領域におけるノウハウを、類似の構造を持つ領域に適用することで容易に解決策が思いついたり、新しい商品のアイデアが思いついたりできるようになるのである。
アナロジー思考の根底にあるのは、やはりモデリングによるシンプル化の技術である。アナロジーを用いるためには、対象の特徴を捉え、余計な部分を除外し、シンプルにモデリングする必要がある。本質的な部分のみを適切なレベル感でモデリングできれば、他の領域からアイデアを持ち込んで適用する場合も効果的に適用できる。

学習する組織




言わずと知れたベストセラー経営書である。本書の前半は、システム思考について書かれている。システム思考の肝は、経営における重要な事象をモデリングし、中長期的変化、挙動パターンを予測することにある。人間は認知的限界から、線形の因果関係のみを短絡的に捉えがちであるが、その背後には、その状況を支配する非線形な因果関係を持つシステムが存在し、ダイナミックに複雑なパターンを生成しているのである。
システム思考においては、複雑な非線形の因果関係のうち、特に重要なフィードバックループにフォーカスし、モデルをよりリッチに表現する。フィードバックループとは、ある要素aが、別の要素bを引き起こすという、a->bという一方向の因果関係だけではなく、b−>aという逆向きの因果関係を考えるものである。それら二つの因果関係がある場合、人間が予測する結果よりはるかにバリエーションに富んだ挙動を示す。自己強化型のループの場合は、指数関数的に増加、減少したり、バランス型のループの場合には振動しながら減衰することもある。

一例を紹介する。Stock and flow diagram of New product adoption model(Wikipediaより引用)の場合、新製品の潜在的なユーザーが、実際に利用するまでの関係性を以下のようなフィードバックループを用いて、記述する。


上記モデルについて、シミュレーションを行うことで、以下のような、ユーザー増加に関する動的特性を観察できる。



現状見えている事象や出来事から、その背後に存在するシステムを見出すには、状況を引き起こしている要素を発見、選択し、フィードバックの関係性を含め、システムの構造を必要十分にモデリングするスキルが肝要となる。「スーパープログラマーに学ぶ 最強シンプル思考術」なども参考にしながら、モデリング自体に慣れ、必要がある。
フィードバックループを含むシステミックなモデルが描けるようになれば、上記のような動的な特性の把握が容易になり、挙動パターンの理解、システム自体の構造的な改革を行えるようになるだろう。

まとめ

今回は、あつかう対象が複雑化し、情報が氾濫する現代に必須のスキルである、モデリングの方法、モデルを基にした思考法に関する書籍を紹介した。今回紹介したようなシンプルに考える方法に興味を持って頂けたら幸いである。



2016年3月19日土曜日

新・観光立国論(デービット・アトキンソン)から考える日本の文化財に関する課題

デービット・アトキンソン
新・観光立国論

雑感

少子高齢化社会という日本の未来に対して、観光立国として生き残る、むしろ勝ち抜くための提言をしている。文章として読みやすいだけではなく、大衆書籍には珍しく、きちんと様々な客観的なデータに基づいて論じられているため、疑問なく筆者の主張がすっと理解できる。日本の行く末を少しでも考えている人であれば、その一つの道として理解しておくべき内容だと思う。

筆者の主張の概要

GDP増加のためには人口増加が不可欠である。しかしながら、少子化を完全に防ぐことや、移民を受け入れることは現実的ではない。そこで、観光客を「短期移民」と位置付け、日本が観光立国となることが現実解であると主張する。
日本は、観光立国としての4条件(「気候」「自然」「文化」「食事」)全てについて基準を満たす稀有な国である。しかし、2013年の観光客数は世界26位の約1000万人に留まり、1位であるフランスの約8500万人と非常に大きい差がある。これは何かしらの原因があるに違いない。後半では、このフランスを初めとする現在の観光大国との相違点に触れ、なぜ現在の日本が観光立国となれていないかを多角的に分析し、どうすれば観光大国となれるのかに言及する。

日本の文化財に関する課題

第6章に日本の文化財に関する問題点が述べられており、そこが日本人の私としても大変共感できた。その部分についてまとめ、簡単に私の意見を述べる。

日本に来る観光客の国別セグメントを考えると、2014年についてはアジアの周辺諸国(台湾、韓国、中国、香港)で訪日観光客数の4位までを占め、5位にアジア圏外のアメリカが初めてランクインする。つまり、先進国が多い欧米諸国からの観光客数が圧倒的に少ないことが大きな問題の一つとして挙げられる。
また、JTB総合研究所のアンケート結果によると、北米、欧州、オセアニアからの観光客が特に「文化・歴史」に関心が高いことがわかる一方で、口コミサイト「TripAdvisor」によると、「世界遺産というから来てみたが、ただの箱だった。」「何がすごいのかわからない」などのコメントが寄せられているようである。

このように外国人へのガイドというのは、やはりその文化の意味合い、歴史的な背景、成り立ち、外国人が耳にして刺激を受けるであろう情報を加えるという「調整」が必要なのです。

つまり、先進諸国の多い欧米からの観光客満足度を向上させる一つの要因として、文化財の歴史的背景や文化財の本質的な意味の説明といった「知的刺激の提供」を行い、知的欲求を満たすことは、大変重要ということである。
 この点については、予てから日本人の私自身も強く課題認識していた部分である。日本人の私が日本の文化財を見てもよくわからないのに、ましてや外国の方がその背景や意味を理解することは極めて入念な事前準備が必要となり、観光どころではなくなってしまう。
 ある文化財がどの素材で作られているだとか、築何年だとかいう客観的な情報も勿論重要な情報であるが、それ以上にその上位の情報を知りたがっているのが国内外問わず、観光客の思いであろう。
 重要文化財であれば、なぜその文化財が重要なのか?歴史的にどのようなイベントがあったのか?また博物館などの展示物であれば、何を目的にその作品を作ったのか?どのような心境で、何を考えて、その作品を作ったのか?といったことを知りたいのである。なぜそのような知的欲求が自分にあるのかは説明しきれない部分もあるが、おそらく背後に存在する歴史的な物語を、論理的な整合性のチェックを含めて、理解し納得したいのだと思う。
 
 一例を挙げると、先日東京国立博物館にてボランティアによる作品紹介ツアーに参加し、幾つかの作品の説明を受けた。その一つに野口小蘋という明治の女流南画家の作品である「春秋山水図屏風」があった。第一印象としては美しい屏風であると思ったが、それ以上屏風のどこをどう見れば良いのかが全然わからない。確かに美しい屏風なのだが、自分自身それ以上の観賞するための観点もなければ、表現をするための語彙も持ち合わせていないのである。
 そのタイミングでボランティアの説明員の方から、「野口小蘋は『帝室技芸員』を拝命した初めての女性であり、全帝室技芸員79名のうち女性は2名しかいない。」という情報を得て、一気にその作者の美術界に与えた歴史的な影響を感じることができた。これらの情報は、作品付近には全く書かれておらず、興味を持って精緻に調べて初めて知り得る情報である。

このように一部の方は当たり前のように知っている、作品の背後に存在する興味深いストーリーを添えて展示することで、国内外含めた顧客満足度は向上するに違いない。このテーマは引き続き考え、東京オリンピックまでに何かできれば良いと思う次第である。

2016年1月10日日曜日

韻を踏むべきなのか?〜「声に出して読みたい韻」を読んで〜




概要

様々なヒット曲を例に挙げて、韻とは何か?どのような韻が良い韻なのか?どのように韻を踏むのか?を著者自身の豊富な経験に基づいて読みやすくまとめた一冊であった。韻について知らない人から、韻の勉強をこれから始めたい方に最適の一冊だと思う。

読んでみて、学んだことと想ったことをいくつか述べる。

韻の善し悪し

本書はまず初めに以下のような良質な韻の特徴を挙げていた。
 ・共通の母音の文字数が大きい
 ・同じ母音の文字列の回数が多い
 (イメージとしては、ゲーム「ぷよぷよ」を想像すると良いように想う。一度に消す数が多い、連鎖が多いとスコアが増加するので。)
 また、さらに以下ができると良い韻といえるようである。
 ・韻を踏む品詞や言語が異なる
 ・汎用性のない言葉で韻を踏む
 
 これらをまとめてみると、
 筆者の考える良質な韻とは、以下の性質を持つものといえるかもしれない。
 ・誰にでも作れる訳ではないという、困難性
 ・今まで作られておらずオリジナリティを含むという、新規性

言語による韻の踏みやすさの違い

日本語と英語の韻の踏みやすさに言及していたことが、大変興味深かった。
 
 日本語の文は末尾が動詞+助動詞などで終わる場合がほとんどである。
 一方で、英語においては、文末に、動詞だけでなく、目的語となる名詞や副詞など、文型や修飾の仕方に応じて様々な単語が存在しうる。
 それ故、文末で韻を踏む際のバリエーションが英語の方が多く、多様な韻が踏みやすい。

 また、日本語は母音の一致率が低く感じてしまいやすいという問題も存在する。
 これは単語ベースで母音の一致する割合を考えた場合、
 英語は1単語に含まれる母音数が比較的少ないため、母音が一致していると、単語全体が一致していることが多い。
 例えば、英語の「me(いー)」と「she(いー)」は母音で考えた場合、これら二つの単語は100%一致しているということができるのに対して、日本語の「わたし(ああい)」「きみ(いい)」は、各々33%と50%が一致しているだけで、あまり美しさを感じない。
 このように、日本語は単語が含む母音数が比較的多いため、母音全てを合致させることは難しく、単語ベースで考えた際に比較的に一致していないような印象を受けやすい。
 
 このように言語特性によって、韻の踏みやすさは異なる。
 
 日本語が比較的韻を踏みにくいことは、必ずしも悪い訳ではなく、制約が強い分、様々な工夫を重ね、色々な技術が生まれたようである。体言止めを基に、文末の言葉のバリエーションを出したり、言語を股がって韻を踏むことでオリジナリティを出したり、創造は尽きない。
 
 今後、様々な言語で詩に潜む韻に着目してみる価値はある。

韻を踏むべきなのか?

「学校へ行こう」の歴史暗記ラップにて一世を風靡したCo.慶応と筆者の対談(「韻タビュー」)が最後に記されている。

ここにきてやっと、Co.慶応の口から韻を踏むことの意味について言及されている。
実は、本書は韻とは何か?どのように韻を踏むか?などの問いには、丁寧に大変わかりやすく答えられているのだが、そもそもなぜ韻を踏む必要があるのか?についての言及がほとんどない。むしろ、筆者は韻が世の中の役に立つなんて、思っていなかったと記されている。

本巻末対談にて、Co.慶応は端的に「強く印象づける」ためのツールとして、「韻を踏む」ことに注力したと話している。実際、歴史などの暗記モノは語呂合わせなどの想起しやすい方法で覚えていくことが常套手段であり、韻を踏むことによって同じ母音の並びで特徴を覚えられれば、定着率が高くなるということは頷ける。

ここで議論になると思われるのは、「強く印象づける」ためのツールとして、韻を踏むことを捉えた場合、韻を踏むために日本語の語順を多少崩して、韻を踏むために不自然な用語を選択し、韻を踏むために・・・というように韻を踏むことに注力するあまり、日本語として理解しにくくなってしまったり、曲との調和が崩れてしまったりしては、逆に印象づかなくなってしまうことが考えられる。

個人的には、上記の調和を崩さない範囲で、韻を踏むことが出来る良い言葉が選択できた場合のみ、比較的わかりやすく韻を踏むことが、最も効果的なのではないかと考える。カタイ韻を踏むことを目的にして、理解しにくい詩にすることは逆効果の方が大きいように考える。

韻を踏むべきなのか?(再考)

しかしながら、詩の至る所で韻が踏まれているというのは、理解できた場合に、大変に気持ちよく、美しさすら感じてしまうことは事実である。個人的な感覚としては、完全数(その数自身を除く約数の和が自身となる数。例. 28=1+2+4+7+14)に近く、全く無駄がない印象を受ける。
この感覚はアートに近いと考えられ、アートの中でも秩序を持った建造物のようなアートだと考える。母音という制約条件を満たしつつ、全体として、幾重にも重なった厚みのある意味を想起させる。

ということで、韻を踏むべきかどうか、どの程度韻を踏むべきか、は目的によって全く異なるが、「韻を踏む」というテクニックは大変に有用であると考えられる。また、様々なアーティストが「韻を踏む」というテクニックを用いることがあるというのを知ることで、アーティストの表現したい「想い」を理解し易くなることは間違いないだろう。

このエントリーをはてなブックマークに追加

2015年6月28日日曜日

日本酒の「キレがある」とは?

日本酒を味わう会

日本酒を味わう会に参加してきた。
各自1本以上の自慢の日本酒(四合瓶/一升瓶)とまいおちょこを持ち寄り、
まいおちょこに注いで、飲んで、味わって、感想を共有しながら、
次々に日本酒を回していく。

今回は30人以上の参加者がいて、一度に40本以上の日本酒を味わうことができた。また、参加者が持ってくる酒は「自慢の」日本酒であり、1つ1つのお酒も入手困難なハイレベルな逸品であるため、大変満足度の高い素晴らしい会であった。



客観的っぽい主観的なコメント

様々な日本酒を味わう中で、自分を含めた参加者は次のようなコメントをそれぞれのお酒について述べていく。


  • 「甘い/辛い」
  • 「キレがある」
  • 「酸が強い」
  • 「ひねがない」

etc...

「おいしい」「苦手」といったコメントは明らかに主観的なコメントであるので、真偽を確かめることができないし、確かめることに意味がない。
しかし、上記のコメントは一見、日本酒に関する客観的な属性について述べているはずなので、正誤が気になってきてしまう。
実際、共感できるコメントもあれば、疑問を感じるコメントや何を意味しているかわからないコメントも多くあった。

では、なぜこのように理解できないコメントが発生するのであろうか。

解せない理由

例えば、「きんきんに冷えていて美味しい」というコメントであれば、
温度に対する情報であることがわかる。

これについて疑問を持つとすれば、「冷えている」というのが絶対的な情報ではなく、あくまで相対的な情報なので、普段から比較的冷たいものを飲んでいる人からすれば、この程度で「きんきんに冷えている」といえるのだろうか、と想ってしまう場合くらいであろう。

つまり、何に関するコメントは理解できるが、その内容が主観的な相対的情報であるため、万人で一致しない場合である。

しかし、「キレがある」「ひねがない」というコメントは、自分のような未熟者からすると、どのような観点についての情報かも正しく認識できない。「キレがある」とは日本酒を口に含めた場合に感じるどの要素がどうであることを意味しているのか・・ということがわからないのである。

まとめると以下のような場合で、コメントが解せない場合がありそうである。

  1. どの観点/軸の情報かわかる if not 何について話しているのかわからない。
  2. ある観点/軸の認知/知分解能が同じ if not  感じ方の粒度が粗いため、細かい差異がわからず、細かい違いに関して再現性がなくなる 
  3. ある観点/軸の感じ方の平均が同じ if not 相対的な表現にズレが生じる。

3.については、飲み比べをしながら、前の日本酒に比べてどうこうといえば問題にならない話である。1,2について、もう少し考えてみる。

共通の表現をどのように得るか

通常、生活の中で共通の「ある表現」「ある言葉」を得るためには、その言葉が何を指しているのかが理解できる必要がある。概念としては認識できているが、その概念に共通の名前がないだけの場合は比較的容易に言葉を得られる。(例えば、英語圏の人との共通の表現を得る場合、「犬」という概念を認識できていれば「dog」がすぐわかる。)

問題となるのは、概念としてそもそも認識できていない場合である。
「キレがある」という表現を獲得しようとした場合、得る方法は2つあると考える。
1つ目は、メタファーを利用するということである。同様の表現を日本酒の世界に引きずり込んで当てはめるという方法である。例えば、ビールの「キレがある」を認識できている人に、日本酒も同じようなものであることを伝えれば(自分は同じようなものかどうかもわからないが、、)、その言葉を獲得できるかもしれない。
2つ目は、「キレがある」群と「キレがない」群を飲み比べることで、学習する方法である。ある程度の分解能があれば、各要素の違いから、「キレ」のあるなしを認識できるようになると想われる。そのためには、キレがあるものとキレがないものを明示した学習利き酒セットが必要であろう。

最後に、分解能をどのように得るかという問題であるが、これはその分野を絞ってとにかく多く経験することが必要である。特定の分野に絞って、多くを経験すると、その分野の中で違いを見極めるポイントが認識できるようになる。例えば、自分は子供の頃、欧米の方の顔の識別が苦手であったが、ハリウッド映画を見たり、実際の欧米の方と出会う中で、顔の違いがわかるようになった。どこで識別しているかは言葉にできないのであるが。また、識別する必要性(映画のストーリーの把握など)が存在すれば、学習のスピードは加速する。