Murayama blog.

プログラミング教育なブログ

書籍:Measure What Mattersを読んでいる

たまには普段書かないことをブログに書いてみます。本日は目標管理、OKRについてです。

私は仕事のやる気とかモチベーションとか、そういうのであまり悩んだことのないタイプです。どんな仕事も手をつけ始めたら面白いし学ぶことは多くあると考えています。蟹座のO型です。

一方で組織として、仕事のやる気やモチベーションを考えたときに、先輩や後輩、同僚が仕事に対して悩んでいるのはたくさん見てきたように思います。過去を振り返ると、上司としての自分は周囲のメンバーを前向きにさせるような発言・行動を取れていたかというと、んー、、微妙かもしれません。

最近はソフトウェア開発の現場で「開発チームとして生産性を高めるにはどうしたら良いか」について考えるようになりました。アジャイル開発、スクラムのような考え方・取り組みには以前から関心はありましたが、今はチームとしての仕事へのやりがい・モチベーションを高める工夫として「OKR」というものに興味を持っています。

本の紹介

Measure What Mattersという本を読んでいます。

まだ半分しか読んでいないですが面白いです。ブログに書こうと思うくらいです。スタートアップの事例としてremind.com(https://www.remind.com/)やnuna.com(https://www.nuna.com/)の事例はとてもわかりやすく、またnuna(社名)の由来にも感動しました。

あとGoogleの話や、ITの有名な人たちがたくさん出てくるので面白いです。名言を連発するアンディ・グローブさんがかっこいいです。

OKRとは

OKR(Objectives and key results)は、目標(Objectives)とその主要な結果(Key Results)を定義し、追跡するためのフレームワークです。"目標(O)"は何を達成すべきかを表現し、"主要な結果(KR)"は目標をどのように達成するかをモニタリングする基準になります。

従来の目標管理は「何を」に焦点をあてるものでしたが、OKRでは「どのように」という具体的な指標を補完することで追跡可能な目標管理を実現します。

また一般的に、OKRは組織においてオープンに共有し誰もが閲覧できる状態とし、1つの目標に対して、3つ,4つ程度の主要な結果(Key Results)を定義するようにします。また目標は実現可能でかつ野心的なものが良いとされていて、四半期に一度くらいのペースで振り返りを行うことを推奨しています。

目標の立て方については書籍「Measure What Matters」の中でも以下のような解説がありました。

「第1に困難な目標の方が、楽な目標より、パフォーマンスを高めるために有効である。第2に、具体性のある困難な目標のほうが、曖昧な文言で書かれた目標より、アウトプットの水準が高くなる。」

具体的にどのように書けばよい?

OKRはシンプルなツールなので、私のような単純な人間(蟹座のO型)はすぐに実践したくなります。そうすると「具体的にどのように目標や指標を記述すればよいのか?」という点に興味がいきます。そこが落とし穴のように思っています。

目標のもたらす失敗

OKRの話から離れて、目標管理について考察してみます。

会社のような組織の中で、年に一度あるいは半年に一度、目標を立てる、といった取り組みはよくあるケースだと思います。それらの目標管理が上手く機能している組織もあるのでしょうが、そうでない組織の方が多いのではないでしょうか。

目標について注意しないといけない点もいくつかあります。

  • 目標の形骸化
    • 立てた目標を放置して、忘れてしまう問題
  • 恣意的な目標変更
    • 組織の目標がころころと変わってしまう問題
  • 暴走する目標
    • 目標達成を優先するあまり、品質を無視してしまう問題

「目標なんて要らないよ」派の人はだいたいこれらの意見に同意するのではないでしょうか。管理できていない目標管理はマイナスに作用することも理解しておかなければいけません。

それで、OKRは具体的にどのように書けばよい?

OKRは目標管理ツールです。

現場に導入しようとすると、どうしても具体的な目標や評価指標の書き方にばかり目がいってしまいます(具体的な書き方は「OKR 書き方」でググればわかります)。

それよりもOKRを導入する目的は何か、そこを理解しておかないとOKRを導入しても失敗してしまうでしょう。

実際、書籍「Measure What Matters」の中でも多くの失敗談が語られています。「OKRは1度失敗したら辞める」というケースも多いようですがそれはアンチパターンで、OKR導入の目的を理解して繰り返し実践していくことで精度を高めていく必要があります。

組織におけるOKR導入の目的

目標は進むべき方向のことです。

話がそれますが、以前見たテレビ番組で林修先生がこんなイラストで説明していたのを覚えています。正しい目標と正しい努力、のような話だったと思います。

f:id:yamasahi:20190109173203p:plain

目標を理解して努力しないと意味がないよ、というお話です。

f:id:yamasahi:20190109173236p:plain

組織だとこんな感じでしょうか。

f:id:yamasahi:20190109173910p:plain

(なんか図がでかくてすみません)

これも完全に余談ですが、漫画キングダムでは飛信隊の旗を立てるシーンが印象的です。目標と旗ってなんか似てるような気がしています。

組織における目標はチームメンバーを鼓舞し、パフォーマンスを改善するものであるべきです。もう一度「Measure What Matters」の中から引用しておきます。

「第1に困難な目標の方が、楽な目標より、パフォーマンスを高めるために有効である。第2に、具体性のある困難な目標のほうが、曖昧な文言で書かれた目標より、アウトプットの水準が高くなる。」

組織にOKRを導入する目的、それはチーム全員が目標を理解し、同じ方向を向いて仕事を進めていくことです。またOKR(目標と主要な評価指標)によって、組織の中で具体的に議論が行われ、協力関係が生まれ、仕事へのモチベーションを高めるいくことが理想です。

そのためにはOKRを補完する仕組みとしてCFR(Conversation, Feedback, Recognition)という考え方もあるります。対話、フィードバック、承認、いずれも組織において大事な言葉です。OKRのようなツールを導入したからといって、すぐに現場が改善されるわけではありません。

今後の話

本が面白かったので勢いで書いてみました。今も読み進めていますが、OKRの導入には以下の4つのキーワードを理解する必要があります。

OKRについて、目標管理や組織におけるモチベーションについて、もう少し勉強を継続してみようと思います。

JavaScript 30-seconds-of-code String編

お仕事で若手エンジニアのみなさんから「エンジニアならブログを書きますよねぇ」という話になり、ネタ探しにGitHubのExploreを覗いていたら30-seconds-of-codeというリポジトリを見つけました。

https://github.com/30-seconds/30-seconds-of-code/tree/master/snippets

30秒で学べるJavaScriptコードスニペット集って感じでしょうか。配列や文字列、関数オブジェクトなどいくつかのカテゴリーにコードがまとめらています。

  • 🔌 Adapter
  • 📚 Array
  • 🌐 Browser
  • ⏱️ Date
  • 🎛️ Function
  • ➗ Math
  • 📦 Node
  • 🗃️ Object
  • 📜 String
  • 📃 Type
  • 🔧 Utility

まずは入りやすそうなStringを見てみました。

String

けっこうな数のコードがあります。いずれも関数として定義します。

  • CSVToArray
  • CSVToJSON
  • README
  • URLJoin
  • byteSize
  • capitalize
  • capitalizeEveryWord
  • compactWhitespace
  • decapitalize
  • escapeHTML
  • escapeRegExp
  • fromCamelCase
  • indentString
  • isAbsoluteURL
  • isAnagram
  • isLowerCase
  • isUpperCase
  • mapString
  • mask
  • pad
  • palindrome
  • pluralize
  • removeNonASCII
  • reverseString
  • sortCharactersInString
  • splitLines
  • stringPermutations
  • stripHTMLTags
  • toCamelCase
  • toKebabCase
  • toSnakeCase
  • toTitleCase
  • truncateString
  • unescapeHTML
  • words

余談ですが、この辺の関数の名前から処理を想像できるのも大事ですよね。padとかmapStringとかtoKebabCaseとか。ケバブ

中にはクイズみたいな面白いものもあります。たとえばpalindrome関数。palindromeは日本語だと"回文"という意味で、文字列を反対から読み上げても同じかどうか検証します。サンプルコードはこんなかんじです。

const palindrome = str => {
  const s = str.toLowerCase().replace(/[\W_]/g, '');
  return s === [...s].reverse().join('');
};
palindrome('taco cat'); // true

String.prototypeにはreverse関数がないので、いったん文字の配列に変換してからreverseしてjoinで文字列として再結合しています。コードをよく見ると、

[...s]

おっとなにこれ、JS詳しくないマンだと拒否反応がでますが、...はスプレッドオペレータというもので文字列に対して使うとs.split("")と同様の結果となります。

s = "taco cat"
"taco cat"
s.split("")
(8) ["t", "a", "c", "o", " ", "c", "a", "t"]
[...s]
(8) ["t", "a", "c", "o", " ", "c", "a", "t"]

他にもキャメルケース変換、スネークケース変換、ケバブケース変換(、、ケバブ?)など、面白い関数がたくさんあります。あと正規表現もバンバン出てくるので、真面目にやれば正規表現の勉強になると思います。あとプログラミング習い始めの人、配列とか関数とかオブジェクトとか理解していた人にはちょうど良いお題のように感じました。まずは無心になって写経するだけでも勉強になると思います。あと技術者のスキル測るのにも使えるんじゃない、、と思ったり。

というわけで原文の英語の方が読みやすい気もしますが、面白そうなので日本語にまとめてみました。

CSVToArray

CSV文字列を2次元配列に置き換えます。

第3引数のomitFirstRowtrueの場合Array.prototype.slice()Array.prototype.indexOf('\n')を使って先頭行を削除します。それからString.prototype.split('\n')を使って個々の行を配列に変換します。第2引数のdelimiterが省略された場合はデフォルトの区切り文字として,を使います。また第3引数が省略された場合はCSV文字列の先頭行(タイトル行)を処理対象として含みます。

const CSVToArray = (data, delimiter = ',', omitFirstRow = false) =>
  data
    .slice(omitFirstRow ? data.indexOf('\n') + 1 : 0)
    .split('\n')
    .map(v => v.split(delimiter));
CSVToArray('a,b\nc,d'); // [['a','b'],['c','d']];
CSVToArray('a;b\nc;d', ';'); // [['a','b'],['c','d']];
CSVToArray('col1,col2\na,b\nc,d', ',', true); // [['a','b'],['c','d']];

CSVToJSON

CSV文字列をオブジェクトの2次元配列に置き換えます。文字列の1行目はタイトル行として処理します。

Array.prototype.slice()Array.prototype.indexOf('\n')String.prototype.split(delimiter)を使って1行目(タイトル行)を取得します。それからString.prototype.split('\n')を使って行ごとの文字列配列を作り、Array.prototype.map()String.prototype.split(delimiter)を使って、個々の行の中の値を取り出します。Array.prototype.reduce()を使って個々の行の値を格納したオブジェクトを作ります。これらのオブジェクトはタイトル行をパースした際のキーを持ちます。第2引数を省略した場合は、デフォルトの区切り文字として, を使います。

const CSVToJSON = (data, delimiter = ',') => {
  const titles = data.slice(0, data.indexOf('\n')).split(delimiter);
  return data
    .slice(data.indexOf('\n') + 1)
    .split('\n')
    .map(v => {
      const values = v.split(delimiter);
      return titles.reduce((obj, title, index) => ((obj[title] = values[index]), obj), {});
    });
};
CSVToJSON('col1,col2\na,b\nc,d'); // [{'col1': 'a', 'col2': 'b'}, {'col1': 'c', 'col2': 'd'}];
CSVToJSON('col1;col2\na;b\nc;d', ';'); // [{'col1': 'a', 'col2': 'b'}, {'col1': 'c', 'col2': 'd'}];

URLJoin

URLのブロックを結合して、妥当なURLに置き換えます。

String.prototype.join('/')を使ってURLのブロックを結合し、一連のString.prototype.replace()と様々な正規表現使って妥当なURL(ダブルスラッシュの排除、プロトコルの適切なスラッシュの追加、パラメータの前のスラッシュの削除、2つ目以降のパラメータを&による結合)に置き換えます。

const URLJoin = (...args) =>
  args
    .join('/')
    .replace(/[\/]+/g, '/')
    .replace(/^(.+):\//, '$1://')
    .replace(/^file:/, 'file:/')
    .replace(/\/(\?|&|#[^!])/g, '$1')
    .replace(/\?/g, '&')
    .replace('&', '?');
URLJoin('http://www.google.com', 'a', '/b/cd', '?foo=123', '?bar=foo'); // 'http://www.google.com/a/b/cd?foo=123&bar=foo'

byteSize

文字列の長さをbytesで返します。

文字列をBlobオブジェクトに変換してsizeプロパティを参照します。

const byteSize = str => new Blob([str]).size;
byteSize('😀'); // 4
byteSize('Hello World'); // 11

capitalize

文字列の先頭文字を大文字に変換します。

配列の分割代入とString.prototype.toUpperCase()を使って先頭の文字を大文字にします。 ...restには先頭文字を除く文字の配列が格納されるのでArray.prototype.join('')を使って再び文字列に復元しています。引数のlowerRestが指定されなかった場合は残りの文字列(先頭文字を除く)をそのまま使います。lowerRestにtrueが指定された場合は残りの文字列を小文字に置き換えます。

const capitalize = ([first, ...rest], lowerRest = false) =>
  first.toUpperCase() + (lowerRest ? rest.join('').toLowerCase() : rest.join(''));
capitalize('fooBar'); // 'FooBar'
capitalize('fooBar', true); // 'Foobar'

capitalizeEveryWord

文字列内の単語ごとの先頭文字を大文字に変換します。

各単語の先頭文字をString.prototype.replace()でマッチさせ、String.prototype.toUpperCase()で大文字に変換します。

const capitalizeEveryWord = str => str.replace(/\b[a-z]/g, char => char.toUpperCase());
capitalizeEveryWord('hello world!'); // 'Hello World!'

compactWhitespace

連続するホワイトスペース文字(スペース、タブ、改ページ、改行)をホワイトスペース文字1文字に置き換えます。

正規表現String.prototype.replace()を使うことで、2文字以上のホワイトスペース文字を1文字に置き換えます。

const compactWhitespace = str => str.replace(/\s{2,}/g, ' ');
compactWhitespace('Lorem    Ipsum'); // 'Lorem Ipsum'
compactWhitespace('Lorem \n Ipsum'); // 'Lorem Ipsum'

decapitalize

文字列の先頭文字を小文字に変換します。

配列の分割代入とString.prototype.toLowerCase()を使って先頭の文字を大文字にします。 ...restには先頭文字を除く文字の配列が格納されるのでArray.prototype.join('')を使って再び文字列に復元しています。引数のupperRestが指定されなかった場合は残りの文字列(先頭文字を除く)をそのまま使います。upperRestにtrueが指定された場合は残りの文字列を大文字に置き換えます。

const decapitalize = ([first, ...rest], upperRest = false) =>
  first.toLowerCase() + (upperRest ? rest.join('').toUpperCase() : rest.join(''));
decapitalize('FooBar'); // 'fooBar'
decapitalize('FooBar', true); // 'fOOBAR'

escapeHTML

HTMLで利用可能な文字列(HTML特殊文字)にエスケープします。

String.prototype.replace()正規表現を使って、マッチした文字をエスケープします。このときコールバック関数を使ってディクショナリ(オブジェクト)によって管理された特殊文字に変換します。

const escapeHTML = str =>
  str.replace(
    /[&<>'"]/g,
    tag =>
      ({
        '&': '&amp;',
        '<': '&lt;',
        '>': '&gt;',
        "'": '&#39;',
        '"': '&quot;'
      }[tag] || tag)
  );
escapeHTML('<a href="#">Me & you</a>'); // '&lt;a href=&quot;#&quot;&gt;Me &amp; you&lt;/a&gt;'

escapeRegExp

正規表現で利用可能な文字列にエスケープします。

String.prototype.replace()を使って特殊文字エスケープします。

Use String.prototype.replace() to escape special characters.
const escapeRegExp = str => str.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
escapeRegExp('(test)'); // \\(test\\)

fromCamelCase

キャメルケースの文字列を変換します。

String.prototype.replace()を使って、キャメルケースを単語の並びに変換します。単語の並びはアンダースコア、ハイフン、スペースなどで補完します。

const fromCamelCase = (str, separator = '_') =>
  str
    .replace(/([a-z\d])([A-Z])/g, '$1' + separator + '$2')
    .replace(/([A-Z]+)([A-Z][a-z\d]+)/g, '$1' + separator + '$2')
    .toLowerCase();
fromCamelCase('someDatabaseFieldName', ' '); // 'some database field name'
fromCamelCase('someLabelThatNeedsToBeCamelized', '-'); // 'some-label-that-needs-to-be-camelized'
fromCamelCase('someJavascriptProperty', '_'); // 'some_javascript_property'

indentString

文字列内の各行をインデントします。

String.prototype.replace正規表現を使って、指定されたインデント幅分、各行の先頭に指定された文字を追加します。第3引数indentが省略された場合デフォルトのインデント文字として' '(半角スペース)を使います。

const indentString = (str, count, indent = ' ') => str.replace(/^/gm, indent.repeat(count));
indentString('Lorem\nIpsum', 2); // '  Lorem\n  Ipsum'
indentString('Lorem\nIpsum', 2, '_'); // '__Lorem\n__Ipsum'

isAbsoluteURL

与えられた文字列が絶対URLの場合にtrue、そうでない場合にはfalseを返します。

正規表現を使って、与えられた文字列が絶対URLか検証します。

const isAbsoluteURL = str => /^[a-z][a-z0-9+.-]*:/.test(str);
isAbsoluteURL('https://google.com'); // true
isAbsoluteURL('ftp://www.myserver.net'); // true
isAbsoluteURL('/foo/bar'); // false

isAnagram

文字列が他の文字列(大文字小文字は無視し、スペース、句読点、特殊文字は除去した文字列)のアナグラム(文字配列の並び替え)であるか検証します。

String.prototype.toLowerCase()と、適切な正規表現を適用したString.prototype.replace()を使って、不要な文字を除去し、 String.prototype.split('')Array.prototype.sort()Array.prototype.join('')を使ってノーマライズ(文字の並びを整理)するnormalize関数を定義します。引数の2つの文字列にnormalize関数を適用して、文字列の並びが等しいか検証します。

const isAnagram = (str1, str2) => {
  const normalize = str =>
    str
      .toLowerCase()
      .replace(/[^a-z0-9]/gi, '')
      .split('')
      .sort()
      .join('');
  return normalize(str1) === normalize(str2);
};
isAnagram('iceman', 'cinema'); // true

isLowerCase

文字列が小文字か検証します。

String.prototype.toLowerCase()を使って、与えられた文字列を小文字に変換し、元の文字列と等しいか比較します。

const isLowerCase = str => str === str.toLowerCase();
isLowerCase('abc'); // true
isLowerCase('a3@$'); // true
isLowerCase('Ab4'); // false

isUpperCase

文字列が大文字か検証します。

String.prototype.toUpperCase()を使って、与えられた文字列を大文字に変換し、元の文字列と等しいか比較します。

Convert the given string to upper case, using String.prototype.toUpperCase() and compare it to the original.

const isUpperCase = str => str === str.toUpperCase();
isUpperCase('ABC'); // true
isLowerCase('A3@$'); // true
isLowerCase('aB4'); // false

mapString

文字列に含まれる個々の文字に対して、指定されたコールバック関数を適用して、新たな文字列を生成します。

String.prototype.split('')Array.prototype.map()を使って、コールバック関数(引数のfn)を文字列内の個々の文字に対して適用します。それからArray.prototype.join('')を使って文字配列を文字列として再結合します。コールバック関数は3つの引数(現在の文字、現在の文字のインデックス、mapString関数の引数に指定された文字列)を受け取ります。

const mapString = (str, fn) =>
  str
    .split('')
    .map((c, i) => fn(c, i, str))
    .join('');
mapString('lorem ipsum', c => c.toUpperCase()); // 'LOREM IPSUM'

mask

文字列を指定されたマスク文字で置き換えます。ただし、文字列の後部については、引数のnumに指定された文字数分はマスクしません。

String.prototype.slice()を使って、アンマスク(マスクしない)する文字列を取り出し、String.prototype.padStart()で、元の文字列の長さ分のマスク文字を追加します。第2引数のnumが省略された場合、デフォルトで4文字のアンマスク文字を確保します。またnumに負の値が指定された場合は、文字列の先頭部分をアンマスクします。第3引数のmaskが省略された場合、デフォルトのマスク文字に*を使います。

const mask = (cc, num = 4, mask = '*') => `${cc}`.slice(-num).padStart(`${cc}`.length, mask);
mask(1234567890); // '******7890'
mask(1234567890, 3); // '*******890'
mask(1234567890, -4, '$'); // '$$$$567890'

pad

対象の文字列が、指定された文字数を下回る場合に、文字列の両側に指定された文字を連結します。

String.prototype.padStart()String.protorype.padEnd()を使って与えられた文字列の両側に文字を連結します。第3引数が省略された場合、半角スペースがデフォルトの文字として連結されます。

const pad = (str, length, char = ' ') =>
  str.padStart((str.length + length) / 2, char).padEnd(length, char);
pad('cat', 8); // '  cat   '
pad(String(42), 6, '0'); // '004200'
pad('foobar', 3); // 'foobar'

palindrome

与えられた文字列が回文になっているか検証します。回文の場合、true、そうでない場合、falseを返します。

String.prototype.toLowerCase()String.prototype.replace()を使って非アルファベット文字を除去して小文字に統一し、それからスプレッドオペレータ(...)を使って文字列を文字の配列に変換し、Array.prototype.reverse()String.prototype.join('')を使って生成した文字列と、元の文字列(非アルファベット文字を除去して小文字に統一したもの)を比較します。

const palindrome = str => {
  const s = str.toLowerCase().replace(/[\W_]/g, '');
  return s === [...s].reverse().join('');
};
palindrome('taco cat'); // true

pluralize

入力された数値によって、基準となる文字列を単数系、あるいは複数形にして返します。第1引数にオブジェクトが指定された場合、関数によって返却されるクロージャを返します。これは"s"による単純な複数形の変換だけでなく、指定されたディクショナリに含まれる複数形単語を返却します。

numが-1か1の場合、単数系の単語を返却します。numがそれ以外の値の場合、複数形の単語を返却します。第3引数のpluralが省略された場合、デフォルトで第2引数のwordに"s"を連結した文字列を複数形の単語として処理するので、必要に応じてカスタマイズすることができます。第1引数のvalがオブジェクトの場合、複数形の単語を格納したディクショナリを保持したクロージャを返却します。

const pluralize = (val, word, plural = word + 's') => {
  const _pluralize = (num, word, plural = word + 's') =>
    [1, -1].includes(Number(num)) ? word : plural;
  if (typeof val === 'object') return (num, word) => _pluralize(num, word, val[word]);
  return _pluralize(val, word, plural);
};
pluralize(0, 'apple'); // 'apples'
pluralize(1, 'apple'); // 'apple'
pluralize(2, 'apple'); // 'apples'
pluralize(2, 'person', 'people'); // 'people'

const PLURALS = {
  person: 'people',
  radius: 'radii'
};
const autoPluralize = pluralize(PLURALS);
autoPluralize(2, 'person'); // 'people'

removeNonASCII

印刷できないASCII文字を除去します。

正規表現を使って印刷できないASCII文字を除去します。

const removeNonASCII = str => str.replace(/[^\x20-\x7E]/g, '');
removeNonASCII('äÄçÇéÉêlorem-ipsumöÖÐþúÚ'); // 'lorem-ipsum'

reverseString

文字列を反転します。 Reverses a string.

スプレッドオペレータ(...)とArray.prototype.reverse()を使って文字列内の文字の並びを逆順にしString.prototype.join('')によって文字を連結して文字列とします。

const reverseString = str => [...str].reverse().join('');
reverseString('foobar'); // 'raboof'

sortCharactersInString

文字列内の文字をアルファベット順にソートします。

スプレッドオペレータ(...)とArray.prototype.sort()String.prototype.localeCompare()を使って文字列内の文字をソートし、String.prototype.join('')を使って再結合します。

const sortCharactersInString = str => [...str].sort((a, b) => a.localeCompare(b)).join('');
sortCharactersInString('cabbage'); // 'aabbceg'

splitLines

複数行の文字列を行の配列に分割します。

String.prototype.split()正規表現を使って行を検出し、配列を生成します。

const splitLines = str => str.split(/\r?\n/);
splitLines('This\nis a\nmultiline\nstring.\n'); // ['This', 'is a', 'multiline', 'string.' , '']

stringPermutations

⚠️ 警告:この関数は文字数が増えると処理時間が指数関数的に増加します。8-10文字を超えると、ブラウザはすべての異なる組み合わせを試みるのでハングするでしょう。

文字列のすべての置換(並び替えた文字列)を生成します(重複を含みます)。

再帰を使います。与えられた文字列の各文字について、残りの文字すべての部分置換を生成します。Use Array.prototype.map()を使って文字と部分置換を連結し、Array.prototype.reduce()によって配列の中のすべての置換を連結します。処理が再帰しますが、文字列が2文字か1文字の場合をベースケースとしています。

const stringPermutations = str => {
  if (str.length <= 2) return str.length === 2 ? [str, str[1] + str[0]] : [str];
  return str
    .split('')
    .reduce(
      (acc, letter, i) =>
        acc.concat(stringPermutations(str.slice(0, i) + str.slice(i + 1)).map(val => letter + val)),
      []
    );
};
stringPermutations('abc'); // ['abc','acb','bac','bca','cab','cba']

stripHTMLTags

文字列からHTML/XMLタグを除去します。

正規表現を使って、文字列からHTML/XMLタグを除去します。

const stripHTMLTags = str => str.replace(/<[^>]*>/g, '');
stripHTMLTags('<p><em>lorem</em> <strong>ipsum</strong></p>'); // 'lorem ipsum'

toCamelCase

文字列をキャメルケースに変換します。

正規表現を使って文字列を単語に分解し、個々の単語の先頭文字を大文字にして結合します。

const toCamelCase = str => {
  let s =
    str &&
    str
      .match(/[A-Z]{2,}(?=[A-Z][a-z]+[0-9]*|\b)|[A-Z]?[a-z]+[0-9]*|[A-Z]|[0-9]+/g)
      .map(x => x.slice(0, 1).toUpperCase() + x.slice(1).toLowerCase())
      .join('');
  return s.slice(0, 1).toLowerCase() + s.slice(1);
};
toCamelCase('some_database_field_name'); // 'someDatabaseFieldName'
toCamelCase('Some label that needs to be camelized'); // 'someLabelThatNeedsToBeCamelized'
toCamelCase('some-javascript-property'); // 'someJavascriptProperty'
toCamelCase('some-mixed_string with spaces_underscores-and-hyphens'); // 'someMixedStringWithSpacesUnderscoresAndHyphens'

toKebabCase

文字列をケバブケースに変換します。

正規表現を使って文字列を単語に分解し、個々の単語を-(ハイフン)をセパレータとして結合します。

const toKebabCase = str =>
  str &&
  str
    .match(/[A-Z]{2,}(?=[A-Z][a-z]+[0-9]*|\b)|[A-Z]?[a-z]+[0-9]*|[A-Z]|[0-9]+/g)
    .map(x => x.toLowerCase())
    .join('-');
toKebabCase('camelCase'); // 'camel-case'
toKebabCase('some text'); // 'some-text'
toKebabCase('some-mixed_string With spaces_underscores-and-hyphens'); // 'some-mixed-string-with-spaces-underscores-and-hyphens'
toKebabCase('AllThe-small Things'); // "all-the-small-things"
toKebabCase('IAmListeningToFMWhileLoadingDifferentURLOnMyBrowserAndAlsoEditingSomeXMLAndHTML'); // "i-am-listening-to-fm-while-loading-different-url-on-my-browser-and-also-editing-xml-and-html"

toSnakeCase

文字列をスネークケースに変換します。

正規表現を使って文字列を単語に分解し、個々の単語を_(アンダースコア)をセパレータとして結合します。

const toSnakeCase = str =>
  str &&
  str
    .match(/[A-Z]{2,}(?=[A-Z][a-z]+[0-9]*|\b)|[A-Z]?[a-z]+[0-9]*|[A-Z]|[0-9]+/g)
    .map(x => x.toLowerCase())
    .join('_');
toSnakeCase('camelCase'); // 'camel_case'
toSnakeCase('some text'); // 'some_text'
toSnakeCase('some-mixed_string With spaces_underscores-and-hyphens'); // 'some_mixed_string_with_spaces_underscores_and_hyphens'
toSnakeCase('AllThe-small Things'); // "all_the_smal_things"
toSnakeCase('IAmListeningToFMWhileLoadingDifferentURLOnMyBrowserAndAlsoEditingSomeXMLAndHTML'); // "i_am_listening_to_fm_while_loading_different_url_on_my_browser_and_also_editing_some_xml_and_html"

toTitleCase

文字列をタイトルケースに変換します。

正規表現を使って文字列を単語に分解し、個々の単語の先頭文字を大文字に変換して半角スペースで結合します。

const toTitleCase = str =>
  str
    .match(/[A-Z]{2,}(?=[A-Z][a-z]+[0-9]*|\b)|[A-Z]?[a-z]+[0-9]*|[A-Z]|[0-9]+/g)
    .map(x => x.charAt(0).toUpperCase() + x.slice(1))
    .join(' ');
toTitleCase('some_database_field_name'); // 'Some Database Field Name'
toTitleCase('Some label that needs to be title-cased'); // 'Some Label That Needs To Be Title Cased'
toTitleCase('some-package-name'); // 'Some Package Name'
toTitleCase('some-mixed_string with spaces_underscores-and-hyphens'); // 'Some Mixed String With Spaces Underscores And Hyphens'

truncateString

指定したサイズで文字列を切り詰めます。

文字列のサイズの上限を指定します。切り詰められた文字列の後部に'...'を連結して返します。

const truncateString = (str, num) =>
  str.length > num ? str.slice(0, num > 3 ? num - 3 : num) + '...' : str;
truncateString('boomerang', 7); // 'boom...'

unescapeHTML

HTML特殊文字をアンエスケープします。

正規表現String.prototype.replace()正規表現を使って、マッチした文字をアンエスケープします。このときコールバック関数を使ってディクショナリ(オブジェクト)によって管理されたアンエスケープ文字に変換します。

const unescapeHTML = str =>
  str.replace(
    /&amp;|&lt;|&gt;|&#39;|&quot;/g,
    tag =>
      ({
        '&amp;': '&',
        '&lt;': '<',
        '&gt;': '>',
        '&#39;': "'",
        '&quot;': '"'
      }[tag] || tag)
  );
unescapeHTML('&lt;a href=&quot;#&quot;&gt;Me &amp; you&lt;/a&gt;'); // '<a href="#">Me & you</a>'

words

文字列を単語の配列に変換します。

指定された正規表現パターン(デフォルトでは非アルファベット文字)とString.prototype.split()を使って、単語文字列の配列に変換します。またArray.prototype.filter()によって空の文字列を除去しています。第2引数が省略された場合はデフォルトの正規表現パターンを使います。

const words = (str, pattern = /[^a-zA-Z-]+/) => str.split(pattern).filter(Boolean);
words('I love javaScript!!'); // ["I", "love", "javaScript"]
words('python, javaScript & coffee'); // ["python", "javaScript", "coffee"]

まとめ

久しぶりのブログ面白かったです。String以外の他のもやってみようと思いました。不定期でGitHubに追記していきます。

https://github.com/murayama333/30-seconds-of-code-ja

Xavierの初期値 Heの初期値の考察

ゼロから作るDeep Learning 第6章を参考に、ニューラルネットワークの隠れ層のアクティベーション(活性化関数の出力)の分布を確認してみます。

次のプログラムは1000件のサンプル(1つのサンプルは100次元のベクトル)を、5層の隠れ層(ノードはすべて100)に流すものです。

import numpy as np
import matplotlib.pyplot as plt

def sigmoid(x):
    return 1 / (1 + np.exp(-x))

def tanh(x):
    return np.tanh(x)

def relu(x):
    return np.maximum(0, x)

def show_activation(activation, weights, ylim=(0, 50000)):

    x = np.random.randn(1000, 100)
    node = 100
    hidden = 5
    activations = {}
    for i in range(hidden):
        if i != 0:
            x = activations[i - 1]
      
        w = weights(node)
        z = np.dot(x, w)
        a = activation(z)
        activations[i] = a
      
    plt.figure(figsize=(18, 4))
    for i, a in activations.items():
        plt.subplot(1, len(activations), i + 1)
        plt.title(str(i + 1) + "-layer")
        plt.hist(a.flatten(), 30, range=(0,1))
        plt.ylim(ylim)
    plt.show()

show_activation(sigmoid, lambda n: np.random.randn(n, n) * 1)
# show_activation(sigmoid, lambda n: np.random.randn(n, n) * 0.01)
# show_activation(sigmoid, lambda n: np.random.randn(n, n) *  np.sqrt(1.0 / n), (0, 10000)) # Xavier

# show_activation(relu, lambda n: np.random.randn(n, n) * 0.01, (0, 7000))
# show_activation(relu, lambda n: np.random.randn(n, n) * np.sqrt(1.0 / n), (0, 7000)) # Xavier
# show_activation(relu, lambda n: np.random.randn(n, n) * np.sqrt(2.0 / n), (0, 7000)) # He

このプログラムでは重みのスケールを標準偏差1のガウス分布としています。

show_activation(sigmoid, lambda n: np.random.randn(n, n) * 1)

以降、重みを変更するとアクティベーションにどのような変化を及ぼすか確認していきます。

標準偏差1の場合

標準偏差1の実行結果は次のようになります。

f:id:yamasahi:20171125231255p:plain

アクティベーションが0と1に偏っているのがわかります。sigmoid関数の出力が0に近くにつれて(あるいは1に近くづにつれて)、その微分の値は0に近づきます。そのため、0と1に偏った分布では逆伝搬での勾配の値が小さくなってしまいます。このような現象は「勾配消失問題(gradient vanishing)」と呼ばれます。

標準偏差0.01の場合

次は重みのスケールを標準偏差0.01のガウス分布としています。

show_activation(sigmoid, lambda n: np.random.randn(n, n) * 0.01)

結果は次のようになります。

f:id:yamasahi:20171125231322p:plain

0.5付近に集中するようになりました。勾配消失問題は解消できましたが、アクティベーションに偏りがあるということは表現力が乏しいということです。複数のニューロンが同じような出力をするのであれば、ニューロンが複数存在する意味が失われてしまいます。「表現力の制限」が問題になってしまいます。

Xavierの初期値の場合

次にXavier Glorotの初期値を試してみます。これは前層のノード数が n の場合 1/sqrt(n) を標準偏差とした分布を使うというものです。

Kerasの場合はglorot_uniform、florot_normalのような初期値が定義されています。

show_activation(sigmoid, lambda n: np.random.randn(n, n) *  np.sqrt(1.0 / n), (0, 10000)) # Xavier

結果は次のようになります。

f:id:yamasahi:20171125231336p:plain

これまでの結果に比べてばらつきのある結果を得ることができました。「勾配消失問題」や「表現力の制限」といった問題を上手く回避できているのがわかります。

ReLU関数の場合

ここまでsigmoid関数のアクティベーションを見てきました。ReLU関数の場合はどうでしょうか。

標準偏差0.01の場合

show_activation(relu, lambda n: np.random.randn(n, n) * 0.01, (0, 7000))

f:id:yamasahi:20171125231354p:plain

Xavierの初期値の場合

show_activation(relu, lambda n: np.random.randn(n, n) * np.sqrt(1.0 / n), (0, 7000)) # Xavier

f:id:yamasahi:20171125231403p:plain

ReLU関数の場合、Xavierの初期値を使ったとしても、層が深くなるにつれて偏りが大きくなります。そこでReLU関数の場合はHeの初期値を使うことでこのようなケースに対処できます。Heの初期値は sqrt(2.0 / n)を標準偏差とするものです。ReLU関数の場合、負の領域がすべて0になるため、ばらつきにより広がりを持たせるために、2倍にすると考えます。

Heの初期値の場合

show_activation(relu, lambda n: np.random.randn(n, n) * np.sqrt(2.0 / n), (0, 7000)) # He

実行結果を見ていましょう。

f:id:yamasahi:20171125231413p:plain

Heの初期値を使えば偏りのないアクティベーションを確認することができました。

Kerasで画像認識 - MNIST編 - ReLU関数

KerasのMNISTのサンプルプログラムについて、活性化関数をsigmoid関数からReLU関数に変更してみましょう。

from keras.models import Sequential
from keras.layers import Dense, Activation
from keras.utils import to_categorical
from keras.datasets import mnist

(x_train, y_train), (x_test, y_test) = mnist.load_data()

# 28x28 => 784
x_train = x_train.reshape(60000, 784)
x_test = x_test.reshape(10000, 784)

# one-hot ex: 3 => [0,0,0,1,0,0,0,0,0,0]
y_train = to_categorical(y_train, 10)
y_test = to_categorical(y_test, 10)

model = Sequential()
model.add(Dense(50, input_dim=784))
model.add(Activation('relu'))
model.add(Dense(20))
model.add(Activation('relu'))
model.add(Dense(10))
model.add(Activation('softmax'))
model.compile(optimizer='sgd', loss='categorical_crossentropy', metrics=['accuracy'])

history = model.fit(x_train, y_train, batch_size=32, validation_data=(x_test, y_test))

プログラムを実行してみると、、学習が上手くいかないようです。

Train on 60000 samples, validate on 10000 samples
Epoch 1/10
60000/60000 [==============================] - 10s - loss: 14.5407 - acc: 0.0977 - val_loss: 14.5353 - val_acc: 0.0982
Epoch 2/10
60000/60000 [==============================] - 7s - loss: 14.5487 - acc: 0.0974 - val_loss: 14.5353 - val_acc: 0.0982
Epoch 3/10
60000/60000 [==============================] - 7s - loss: 14.5487 - acc: 0.0974 - val_loss: 14.5353 - val_acc: 0.0982
Epoch 4/10
60000/60000 [==============================] - 6s - loss: 14.5487 - acc: 0.0974 - val_loss: 14.5353 - val_acc: 0.0982
Epoch 5/10
60000/60000 [==============================] - 6s - loss: 14.5487 - acc: 0.0974 - val_loss: 14.5353 - val_acc: 0.0982
Epoch 6/10
60000/60000 [==============================] - 6s - loss: 14.5487 - acc: 0.0974 - val_loss: 14.5353 - val_acc: 0.0982
Epoch 7/10
60000/60000 [==============================] - 7s - loss: 14.5487 - acc: 0.0974 - val_loss: 14.5353 - val_acc: 0.0982
Epoch 8/10
60000/60000 [==============================] - 6s - loss: 14.5487 - acc: 0.0974 - val_loss: 14.5353 - val_acc: 0.0982
Epoch 9/10
60000/60000 [==============================] - 7s - loss: 14.5487 - acc: 0.0974 - val_loss: 14.5353 - val_acc: 0.0982
Epoch 10/10
60000/60000 [==============================] - 7s - loss: 14.5487 - acc: 0.0974 - val_loss: 14.5353 - val_acc: 0.0982

入力データの正規化

MNISTの画像ベクトルは0-255の値が格納されています。学習が上手く進むようにこの値を0-1の値に変換しておきます。

# 0-255 => 0-1
x_train = x_train.astype('float32')
x_test = x_test.astype('float32')
x_train /= 255
x_test /= 255

このような作業を正規化といいます。

プログラムを次のように実装して再度実行してみましょう。

from keras.models import Sequential
from keras.layers import Dense, Activation
from keras.utils import to_categorical
from keras.datasets import mnist

(x_train, y_train), (x_test, y_test) = mnist.load_data()

# 28x28 => 784
x_train = x_train.reshape(60000, 784)
x_test = x_test.reshape(10000, 784)

# 0-255 => 0-1
x_train = x_train.astype('float32')
x_test = x_test.astype('float32')
x_train /= 255
x_test /= 255

# one-hot ex: 3 => [0,0,0,1,0,0,0,0,0,0]
y_train = to_categorical(y_train, 10)
y_test = to_categorical(y_test, 10)

model = Sequential()
model.add(Dense(50, input_dim=784))
model.add(Activation('relu'))
model.add(Dense(20))
model.add(Activation('relu'))
model.add(Dense(10))
model.add(Activation('softmax'))
model.compile(optimizer='sgd', loss='categorical_crossentropy', metrics=['accuracy'])

history = model.fit(x_train, y_train, batch_size=32, validation_data=(x_test, y_test))

プログラムの実行結果は次のようになります。

Train on 60000 samples, validate on 10000 samples
Epoch 1/10
60000/60000 [==============================] - 6s - loss: 0.6895 - acc: 0.8073 - val_loss: 0.3421 - val_acc: 0.9029
Epoch 2/10
60000/60000 [==============================] - 6s - loss: 0.3159 - acc: 0.9085 - val_loss: 0.2759 - val_acc: 0.9218
Epoch 3/10
60000/60000 [==============================] - 8s - loss: 0.2626 - acc: 0.9243 - val_loss: 0.2375 - val_acc: 0.9306
Epoch 4/10
60000/60000 [==============================] - 14s - loss: 0.2282 - acc: 0.9338 - val_loss: 0.2081 - val_acc: 0.9388
Epoch 5/10
60000/60000 [==============================] - 8s - loss: 0.2027 - acc: 0.9413 - val_loss: 0.1904 - val_acc: 0.9437
Epoch 6/10
60000/60000 [==============================] - 7s - loss: 0.1833 - acc: 0.9469 - val_loss: 0.1774 - val_acc: 0.9468
Epoch 7/10
60000/60000 [==============================] - 6s - loss: 0.1682 - acc: 0.9520 - val_loss: 0.1669 - val_acc: 0.9516
Epoch 8/10
60000/60000 [==============================] - 7s - loss: 0.1560 - acc: 0.9541 - val_loss: 0.1575 - val_acc: 0.9531
Epoch 9/10
60000/60000 [==============================] - 6s - loss: 0.1452 - acc: 0.9578 - val_loss: 0.1476 - val_acc: 0.9548
Epoch 10/10
60000/60000 [==============================] - 6s - loss: 0.1360 - acc: 0.9606 - val_loss: 0.1412 - val_acc: 0.9563

今度は上手く学習できているようです。前回sigmoid関数で実行した場合は90%程度でしたので5%近く結果は向上したようです。

Heの初期値

KerasはDenseレイヤーの重みの初期化にglorot_uniform(Glorot(Xavier)の一様分布)を返します。sigmoid関数の場合はGlorotが良いようですが、ReLU関数を使う場合、He の正規分布を使うのが良いとされています。こちらも試してみましょう。

重みの初期化は次のように実装します。

from keras.initializers import he_normal

model.add(Dense(20, kernel_initializer=he_normal()))

利用可能な初期値はKerasのマニュアルページが参考になります。

https://keras.io/ja/initializers/

先ほどのプログラムを修正してみましょう。

from keras.models import Sequential
from keras.layers import Dense, Activation
from keras.utils import to_categorical
from keras.datasets import mnist
from keras.initializers import he_normal

(x_train, y_train), (x_test, y_test) = mnist.load_data()

# 28x28 => 784
x_train = x_train.reshape(60000, 784)
x_test = x_test.reshape(10000, 784)

# 0-255 => 0-1
x_train = x_train.astype('float32')
x_test = x_test.astype('float32')
x_train /= 255
x_test /= 255

# one-hot ex: 3 => [0,0,0,1,0,0,0,0,0,0]
y_train = to_categorical(y_train, 10)
y_test = to_categorical(y_test, 10)

model = Sequential()
model.add(Dense(50, input_dim=784, kernel_initializer=he_normal()))
model.add(Activation('relu'))
model.add(Dense(20, kernel_initializer=he_normal()))
model.add(Activation('relu'))
model.add(Dense(10, kernel_initializer=he_normal()))
model.add(Activation('softmax'))
model.compile(optimizer='sgd', loss='categorical_crossentropy', metrics=['accuracy'])

history2 = model.fit(x_train, y_train, batch_size=32, validation_data=(x_test, y_test))

プログラムの実行結果は次のようになります。

Train on 60000 samples, validate on 10000 samples
Epoch 1/10
60000/60000 [==============================] - 12s - loss: 0.7723 - acc: 0.7700 - val_loss: 0.3472 - val_acc: 0.8991
Epoch 2/10
60000/60000 [==============================] - 15s - loss: 0.3223 - acc: 0.9079 - val_loss: 0.2798 - val_acc: 0.9179
Epoch 3/10
60000/60000 [==============================] - 9s - loss: 0.2673 - acc: 0.9229 - val_loss: 0.2414 - val_acc: 0.9291
Epoch 4/10
60000/60000 [==============================] - 6s - loss: 0.2325 - acc: 0.9336 - val_loss: 0.2169 - val_acc: 0.9371
Epoch 5/10
60000/60000 [==============================] - 8s - loss: 0.2070 - acc: 0.9406 - val_loss: 0.1937 - val_acc: 0.9427
Epoch 6/10
60000/60000 [==============================] - 14s - loss: 0.1872 - acc: 0.9458 - val_loss: 0.1786 - val_acc: 0.9484
Epoch 7/10
60000/60000 [==============================] - 9s - loss: 0.1706 - acc: 0.9509 - val_loss: 0.1644 - val_acc: 0.9520
Epoch 8/10
60000/60000 [==============================] - 9s - loss: 0.1561 - acc: 0.9547 - val_loss: 0.1552 - val_acc: 0.9542
Epoch 9/10
60000/60000 [==============================] - 9s - loss: 0.1443 - acc: 0.9582 - val_loss: 0.1445 - val_acc: 0.9576
Epoch 10/10
60000/60000 [==============================] - 10s - loss: 0.1344 - acc: 0.9611 - val_loss: 0.1369 - val_acc: 0.9589

若干の改善はありませんが、大きな変化はみられませんでした。今回のMNISTデータだと効果がわかりにくいのかもしれません。重みの初期化についてはまた時間のあるときに考察してみようと思います。

Kerasで画像認識 - MNIST編

Kerasを使った画像認識のプログラムです。有名なMNISTデータ(手書き数字)を使ったものです。

MNIST handwritten digit database, Yann LeCun, Corinna Cortes and Chris Burges

from keras.models import Sequential
from keras.layers import Dense, Activation
from keras.utils import to_categorical
from keras.datasets import mnist

(x_train, y_train), (x_test, y_test) = mnist.load_data()

# 28x28 => 784
x_train = x_train.reshape(60000, 784)
x_test = x_test.reshape(10000, 784)

# one-hot ex: 3 => [0,0,0,1,0,0,0,0,0,0]
y_train = to_categorical(y_train, 10)
y_test = to_categorical(y_test, 10)

model = Sequential()
model.add(Dense(50, input_dim=784))
model.add(Activation('sigmoid'))
model.add(Dense(20))
model.add(Activation('sigmoid'))
model.add(Dense(10))
model.add(Activation('softmax'))
model.compile(optimizer='sgd', loss='categorical_crossentropy', metrics=['accuracy'])

history = model.fit(x_train, y_train, batch_size=32, validation_data=(x_test, y_test))

コードを書いて実行してみましょう。機械学習の開発環境にはJupyter Notebookがオススメです。

murayama.hatenablog.com

実行結果は次のようになります。テストデータで91%の正答率(val_acc)です。

Downloading data from https://s3.amazonaws.com/img-datasets/mnist.npz
10862592/11490434 [===========================>..] - ETA: 0sTrain on 60000 samples, validate on 10000 samples
Train on 60000 samples, validate on 10000 samples
Epoch 1/10
60000/60000 [==============================] - 6s - loss: 1.8023 - acc: 0.5735 - val_loss: 1.3870 - val_acc: 0.7524
Epoch 2/10
60000/60000 [==============================] - 5s - loss: 1.0939 - acc: 0.8008 - val_loss: 0.8456 - val_acc: 0.8457
Epoch 3/10
60000/60000 [==============================] - 5s - loss: 0.7209 - acc: 0.8618 - val_loss: 0.6233 - val_acc: 0.8767
Epoch 4/10
60000/60000 [==============================] - 5s - loss: 0.5610 - acc: 0.8814 - val_loss: 0.5006 - val_acc: 0.8945
Epoch 5/10
60000/60000 [==============================] - 5s - loss: 0.4770 - acc: 0.8892 - val_loss: 0.4278 - val_acc: 0.8998
Epoch 6/10
60000/60000 [==============================] - 6s - loss: 0.4267 - acc: 0.8960 - val_loss: 0.3993 - val_acc: 0.9026
Epoch 7/10
60000/60000 [==============================] - 8s - loss: 0.4097 - acc: 0.8970 - val_loss: 0.3959 - val_acc: 0.8984
Epoch 8/10
60000/60000 [==============================] - 6s - loss: 0.3875 - acc: 0.9016 - val_loss: 0.3856 - val_acc: 0.9044
Epoch 9/10
60000/60000 [==============================] - 5s - loss: 0.3673 - acc: 0.9035 - val_loss: 0.3514 - val_acc: 0.9076
Epoch 10/10
60000/60000 [==============================] - 5s - loss: 0.3458 - acc: 0.9070 - val_loss: 0.3296 - val_acc: 0.9112

初回実行時はMNISTデータのダウンロードが発生します。そのあと10回の学習が進んでいるのがわかります。

プログラムの解説

プログラムの詳細を見てみましょう。Kerasを使えばMNISTデータもKerasのAPIでダウンロードできます。

(x_train, y_train), (x_test, y_test) = mnist.load_data()

# 28x28 => 784
x_train = x_train.reshape(60000, 784)
x_test = x_test.reshape(10000, 784)

# one-hot ex: 3 => [0,0,0,1,0,0,0,0,0,0]
y_train = to_categorical(y_train, 10)
y_test = to_categorical(y_test, 10)

ここではダウンロード後のデータを全結合型のニューラルネットワークで処理できるようにデータを整形しています。

今回のプログラムは全結合型のニューラルネットワークです。KerasのDenseクラスで全結合レイヤーを作っています。

model.add(Dense(50, input_dim=784))

入力層のノード数はinput_dimで指定します。MNISTの画像データが28x28だから784になります。あと今回は0-9の10クラス分類なので出力層のノード数も10になります。

あとは活性化関数を指定して、レイヤーを並べています。中間層の活性化関数にはsigmoid関数、出力層の活性化関数にはsoftmax関数を指定しています。今回は多クラス分類なので出力層の活性化関数にはsoftmax関数を使っています。

モデルが完成したらコンパイルします。コンパイル時には損失関数とオプティマイザを指定します。損失関数には"categorical_crossentropy"、オプティマイザにはSGDを指定しています。metricsに指定した内容はエポック時に表示したい内容です。ここでは正答率(acc)を表示しています。

model.compile(optimizer='sgd', loss='categorical_crossentropy', metrics=['accuracy'])

ちなみに多クラス分類ではなく2値分類(yes/noみたいな)の場合は損失関数(loss)にbinary_crossentropyを指定します。その場合出力層の活性化関数をsigmoidにもできます。

あとは学習開始です。

history = model.fit(x_train, y_train, batch_size=32, validation_data=(x_test, y_test)))

MNISTの訓練データは60000件(テストデータは10000件)あるので、32件ずつランダムに取り出して勾配を求めます。求めた勾配よって重みが各ノードの重み・バイアスが更新されます(SGD)。引数にvalidation_dataを指定することでエポックごとにテストデータで検証(ホールドアウト検証)してくれます。デフォルトで10エポック(同じことを10回)学習します。

学習のグラフ化

matplotlibを使って学習の様子をグラフにしてみましょう。

import matplotlib.pyplot as plt

plt.ylim(0.0, 1)
plt.plot(history.history['acc'], label="acc")
plt.plot(history.history['val_acc'], label="val_acc")
plt.legend()

plt.show()

f:id:yamasahi:20171125154400p:plain

参考

オプティマイザについては以下のページが参考になります。

postd.cc

ImageNetから画像データをダウンロードする方法

機械学習、画像認識を始めると「大量の画像データないかなー」とググることになります。そうするとすぐにImageNetなる存在に気づきます。

ImageNet

ImageNetとはスタンフォード大学がインターネット上から画像を集め分類したデータセット。一般画像認識用に用いられる。ImageNetを利用して画像検出・識別精度を競うThe ImageNet Large Scale Visual Recognition Challenge(ILSVRC)などコンテストも開かれる。(:AI白書より引用)

ILSVRC、2012年のコンテストでディープラーニングを使ったチームが圧勝した話も有名です。

それでImageNetのサイトを訪問すると、画像を検索して、そんで画像選んで、ダウンロードしようとするとURLの一覧が表示されてー。え。これからどうしようとなります。一括ダウンロードするサンプルプログラムもネット上にいくつかありますが、Python2系のものだったり、すぐに動かなかったので自分の勉強用に作ってみました。Anacondaとか入れておいて必要なライブラリが揃っていれば動くと思います。

ダウンロードできる画像は、著作権フリーというのではないので、自己学習用のものです。

import sys
import os
from urllib import request
from PIL import Image

def download(url, decode=False):
    response = request.urlopen(url)
    if response.geturl() == "https://s.yimg.com/pw/images/en-us/photo_unavailable.png":
        # Flickr :This photo is no longer available iamge.
        raise Exception("This photo is no longer available iamge.")

    body = response.read()
    if decode == True:
        body = body.decode()
    return body

def write(path, img):
    file = open(path, 'wb')
    file.write(img)
    file.close()

# see http://image-net.org/archive/words.txt
classes = {"apple":"n07739125", "banana":"n07753592", "orange":"n07747607"}

offset = 0
max = 10
for dir, id in classes.items():
    print(dir)
    os.makedirs(dir, exist_ok=True)
    urls = download("http://www.image-net.org/api/text/imagenet.synset.geturls?wnid="+id, decode=True).split()
    print(len(urls))
    i = 0
    for url in urls:
        if i < offset:
            continue
        if i > max:
            break

        try:
            file = os.path.split(url)[1]
            path = dir + "/" + file
            write(path, download(url))
            print("done:" + str(i) + ":" + file)
        except:
            print("Unexpected error:", sys.exc_info()[0])
            print("error:" + str(i) + ":" + file)
        i = i + 1

print("end")

imagenet.pyとかで保存して、

python imagenet.py

みたいにプログラムを実行すると apple, banana, orange 3種類の画像をダウンロードしてきます。カレントフォルダ上にapple, banana, orangeフォルダができます。

apple, banana, orange以外の画像を集めたい場合は以下の部分を編集してみてください。

# see http://image-net.org/archive/words.txt
classes = {"apple":"n07739125", "banana":"n07753592", "orange":"n07747607"}

ImageNet上にwords.txtというファイルがあり、そこに画像分類ごとのIDみたいなのが振られています。

それからダウンロードする枚数は以下の部分で調整できます。

offset = 0
max = 10

なんとなく見た感じだと、apple, banana, orangeだと1000枚程度はあるので、maxを2000くらいにしておけばそれなりにデータが揃います。offsetは続きからダウンロードしたいとき用です。

あとImageNetからの画像のリンク先がFlickrであることが多く、公開停止になっているリンクも多いです。その場合、エラー画像をダウンロードしてしまうので、その部分を回避するコードです。

response = request.urlopen(url)
if response.geturl() == "https://s.yimg.com/pw/images/en-us/photo_unavailable.png":
  # Flickr :This photo is no longer available iamge.
  raise Exception("This photo is no longer available iamge.")

AIにおける知的財産の考え方について

「AI白書2017 3.2 知的財産」の自分用まとめです。AIの活用シーンにおける以下の3点についてまとめます。

  • AI生成物
  • 学習済みモデル
  • 学習用データ

f:id:yamasahi:20171110180510p:plain

注意:以下に示す内容は現在も議論されている段階です。

AI生成物の著作権保護

音楽や文学作品などのコンテンツを学習することで、新たな創作が可能になりつつあります。たとえばAIを活用して生成された音楽コンテンツには次のようなものがあります。

このようなAI生成物の著作権の取り扱いについて「次世代知財システム検討委員会報告書」には以下の記載があります。

AI生成物を生み出す過程において、学習済みモデルの利用者に創作意図があり、同時に、具体的な出力であるAI生成物を得るための創作的寄与があれば、利用者が思想感情を創作的に表現するための「道具」としてAIを使用して当該AI生成物を生み出したものと考えられることから、当該AI生成物には著作物性が認められその著作者は利用者となる

上記は著作権が認められるケースです。「創作意図」があるかどうかがポイントになるようです。

一方で、利用者の寄与が、創作的寄与が認められないような簡単な指示に留まる場合(AIのプログラムや学習済みモデルの作成者が著作者となる例外的な場合を除く)、当該AI生成物は、AIが自律的に生成した「AI創作物」であると整理され、現行の著作権法上は著作物と認められないこととなる

AIが自律的に生成した成果物は、著作物に該当せず著作権も発生しないと考えられます。

たとえば先のDaddy' Carは、作曲家(Benoit Carre氏)が「ビートルズ風」というスタイルと曲の長さを指定して生成し、作詞・編曲を行っているため、創作的寄与があると考えられます。つまり著作権が発生すると考えられます。

一方で、lamusによる楽曲はAIのみで自律的に制作されているそうです。そのため「AI創作物」として著作権は認められない、と考えられます。

学習済みモデルの保護

学習済みのモデルはプログラムとパラメータで構成されており、著作権法上の「プログラムの著作物」に該当するか議論されています。仮に該当しないとしても、特許法上では「プログラム等」に該当するならば、特許法の要件を満たすなら保護される可能性があります。

また上記に該当しない場合でも不正競争防止法条の秘密管理性、有用性、非公知性といった要件を満たす場合は「営業秘密」として保護されるようです。

(正直、この辺は専門外なのでちょっと難しい。。)

蒸留モデル

学習済みモデルに対して、データの入出力を繰り返し、その結果を別の学習モデルに学習させることもできます。このように作られたものは「蒸留モデル」と呼ばれます。

蒸留モデルは、元のモデルからの依拠性を証明することが難しく、著作権による保護が困難になります。一方で、特許権による保護は、依拠性の立証がなくても認められるため、特許権の範囲での対応も議論されています。

また学習済みモデルの利用規約により、蒸留モデルを禁止する等の契約で保護することもできます。この場合契約当事者以外の第三者から守ることはできないものの、柔軟な対応が可能となります。

学習データについて

AI白書には以下の一文があります。

インターネット上のデータ等の著作物を元に学習用データを作成・解析することは営利目的の場合も含めて、著作権法47条の7に基づいて著作権侵害には当たらないとされており、機械学習活用の促進にとって我が国特有の有用な制度となっている。(AI白書より)

学習データについては著作権法47条の7がポイントのようです。以前、こちらの記事も話題になっていました。

www.itmedia.co.jp

日本はインターネット上のデータを機械学習に活用しやすい一方で、AIの研究開発推進に向けての国産の共有データセット(ImageNetのようなもの)の整備が遅れているとの指摘もあります。

それから著作権法47条の7の規定は、もともと機械学習の促進を想定したものではないため、解釈の仕方や議論の余地が残っています。

共有データセットについての問題

海外にはImageNetやMNIST、MS COCOといった共有データセットが存在します。また、これらのデータを活用して、事前に学習済みのモデルも公開されています。

これらの学習済みモデルは、欧米で作られた共有データセットで作られている点を理解しておく必要があります。そのため日本固有の「ラーメン」のような画像認識ができない、といった問題があります。

参考書籍

AI白書 2017

AI白書 2017