IT・技術研修ならCTC教育サービス

サイト内検索 企業情報 サイトマップ

研修コース検索

コラム

スーパーエンジニアの独り言

CTC 教育サービス

 [IT研修]注目キーワード   Python  UiPath(RPA)  最新技術動向  Microsoft Azure  Docker  Kubernetes 

第96回 エヴリシング・アイ・ウォンテッド (藤江一博) 2020年3月

見上げると歪んだ光がゆらゆらと降り注いでいた。
気が付くと水面(みなも)が遥か上にあった。
太陽光が水面下で屈折して何かしらの虚像を描いている。
水中では光の速度が空気中と違うため虚像を映し出すのだろう。
風がなびいて水面に波紋が出来ると太陽光が揺らいで降り注いでくる。
ゆらゆらと柔らかい光に包み込まれてなんだか暖かく感じる。
どうやら海の底にいるようだ。

 
 
 

「シックス・フィート・アンダー」:

鍵盤のリバーブした単音が少し歪んで揺らぎながら並んでいる様なハモンドオルガンの音色にも似た緩やかなイントロダクション。
イントロのスピードに呼応しながらゆっくりと喋り出して独り言のように呟いているよう伝える「ビリー・アイリッシュ」"Billie Eilish" の声。
彼女が囁くように唄う「エヴリシング・アイ・ウォンテッド」"everything i wanted" がヘビロテです。
彼女の夢の内容をあたかも客観的に考察しているかに思わせながら内省的に独白する歌詞です。

最新の音楽にちょっと疎いのですが、「バッド・ガイ」"bad guy" が大ヒットした「ビリー・アイリッシュ」"Billie Eilish" は、ティーンエイジャーで成功した歌手として知っていました。

「シックス・フィート・アンダー」"six feet under" 辺りからその名前は聞き及んでいて、「コピーキャット」"copycat"、「アイドンワナビーユーエニモア」"idontwannabeyouanymore"、「ビッチズ・ブロークン・ハーツ」"bitches broken hearts"、などなど次々とソーシャル・ネットワークを駆使して発表していました。
これら初期発表曲をまとめたミニ・アルバム「ドント・スマイル・アット・ミー」"dont smile at me" で若い女性リスナーを虜にしましたが、彼女の楽曲だけでなく言動やファッションなど彼女のスタイルを含めたアイコン的な存在に昇華しています。
チャートを駆け上がった「バッド・ガイ」"bad guy" 以降にリリースされた楽曲では、映画「ROMA」からインスパイアされたコンピレーションでは「ホエン・アイ・ワズ・オールダー」"When I Was Older (Music Inspired By The Film ROMA)" で参加するなど多方面で認められ、その後も「エヴリシング・アイ・ウォンテッド」"everything i wanted"、「ノー・タイム・トゥ・ダイ」"no time to die" 、佳曲のリリースが現在進行形で続いています。

 
 
 

「エヴリシング・アイ・ウォンテッド」:

その中で「エヴリシング・アイ・ウォンテッド」"everything i wanted" がヘビロテになったのは、毎週欠かさずオンエアーをチェックしているテレビ番組の小林克也さんプレゼンツ「ベストヒットUSA」を観ていて、ヒットチャートの中から「ビリー・アイリッシュ」"Billie Eilish" を取り上げてくれたからです。
放送されたのが「エヴリシング・アイ・ウォンテッド」"everything i wanted" のビデオで、視聴させて頂いてとても良かったという感想がヘビロテの理由です(以前のコラム『第90回 イングランド・ロスト』もご覧ください)。

小林克也さんがこのプロモーションビデオを流す前に解説してくれました。

この曲のエピソードとして「ビリー・アイリッシュ」が「ゴールデンゲートブリッジから飛び下りたのに誰も私のことを気づいてくれなかった」という「悪夢」を題材にした「ナイトメア」"nightmare" という歌を創ろうとしたら、お兄ちゃんが「そんなことはないよ。」というように励ましてくれたのだそうです。
お兄さんの助言で後ろ向きな「ナイトメア」"nightmare" というタイトルが前向きな「エヴリシング・アイ・ウォンテッド」"everything i wanted" に変わったそうです。

もう一つのエピソードは、これから放映するビデオ(筆者注釈:映像では黒い自動車 [車種不明、但し「ロータス・エスプリ」"Lotus Esprit" ではない] が、海に潜っていくシーン)では、二番の歌い出しは実際に水の中で唄ったものを録音したらしいので注目して聴いてくださいと、克也さんが仰っていました。

「ベストヒットUSA」放送後も気に入ったこの曲のプロモーションビデオを何度も繰り返し観ているのですが、小林克也さんの解説通りでした。
ビデオ冒頭に「お兄ちゃんが親友」だというこのような謝辞から始まっていたのです。

"finneas is my brother and my best friend. no matter the circumstance, we always have and always will be there for each other."

そしてビデオ最後のテロップまで見て頂けると、彼女自身が監督していることも知り得ます。

"directed by billie eilish"

この "everything i wanted" のミュージックビデオでは、ロードムービーの如く「黒い車(車種不明)」だけでストーリーが完結します。
そして最後のテロップが表示される前、画面が光の届かない深海の色である黒く塗りつぶされていく少し前のラストシーン映像では、この「黒い車」がゆっくりと沈んでいって海の底に柔らかい砂の上に置かれて「黒」と同化していくところで「終劇」となります。

 
 
 

「バッド・ガイ」:

この「黒い車」を観て想い出したのが、「ゴリラズ」"Gorillaz" の「スタイロ」"Stylo" です。
こっちのビデオは「マッドマックス 2」"Mad Max2:The Road Warrior" をリスペクト(模倣)したバージョンで真っすぐで平坦な道路をブロロロロと爆音まき散らして疾走します。
ビデオには「ブルース・ウィリス」"Bruce Willis" が出演して盛り上げてくれています。

こちらの「黒い車」はパトカーの改造車みたいでルーフトップが白いです。車種は「シボレー・カマロ」"1986 Chevrolet Camaro first generation"。
こちらのビデオでは、「ブルース・ウィリス」"Bruce Willis" に追い詰められた「黒い車」は断崖絶壁の崖の上から海へと落ちて行きます。
ですが、この車は潜水機能があって「黒い鮫」に変形した潜水艦になります。スイスイ泳いで「終劇」です。

もう一つ想い出した「黒い車」は、「ダフト・パンク」"Daft Punk" の「ウィジン」"Within" です。
「ウィジン」"Within" のビデオでは、楽曲が淡泊なので「妖怪人間ベム」のようなストーリー仕立てに少しだけなっています。
物語は「黒い車」で「自分探し」の旅に出ましたというテーマで、色々葛藤がありまして最後に虚しさが残りまして「終劇」です。
「黒い車」の車種は「フェラーリ 412」"1987 Ferrari 412" 。
黒の外装にベージュの内装、とても渋くて最高にカッコイイです。
少しフォルムが「ロータス・エスプリ」"Lotus Esprit" に似ていますが、こっちの黒い車は海には潜りません。

このビデオで実際に使用した「フェラーリ 412」は、後日チャリティーに出品されました。
こちらの黒い車には潜水機能も無くて海には潜れませんが、なんと一億円を越える高価格で落札されたらしいです。
そしてこのチャリティーは、「東日本大震災復興支援」のために「日本赤十字社」へ寄付するチャリティー・オークションだったのです。

 
 
 

「ありがとう、ダフト・パンク」"merci beaucoup, Daft Punk."

 
 
 

「ドント・スマイル・アット・ミー」:

最近、「アルゴリズム」の勉強を始めました。

「プログラマ」だと言い張っても何かの役に立てなければ意味がありません。
問題を解決できるようなプログラムを書けなければ役に立ちません。
相棒(直人)の影響ですが、何か役に立てるような人になりたいのです。
そこで基本からということで「アルゴリズム」を一から勉強しようと思い立ったが吉日です。

簡単な問題から解いて覚えようと思いまして、先ずは「手頃な」問題探しから始めました。
そこで、片っ端から「問題探し」をしていたのですが、どうも「手頃な」問題はありません。

ちょっと見かけた「簡単そうな」問題でも手が出ないのです。
触手が動かないのではなくて、手が動いてくれないのです。
一番、簡単そうな問題でも頭が痛くなりそうです。

これは、ある程度の習熟を図るためには相当な時間が掛かりそうだとすぐに解りました。
じっくり腰を据えてかからなければと少し覚悟を決めたのです。
何か出来るようにならないものかと、そう思ったからです。
何者になれるのかはわかりませんが、何者かになりたいのです。
「何者になりたいのか」というイメージがないままに、それを目指すのは辛いのですが現在よりは前進出来れば良いという考えです。
兎に角、「見る前に跳べ」です。

 
 
 

「アイドンワナビーユーエニモア」:

先ずは「問題」探しから始めました。
手あたり次第にアルゴリズムの書籍を買い漁りました。
書籍を走り読みしてみたのですが、どれも手が動きそうにありません。

どんな問題ですら難問なので適当な難易度を探すのを諦めかけたのですが、何でも良いと開き直って「この指止まれ」で決めたのが「数学パズル」という書籍に掲載されていた「四つの数字を計算する」という問題です。

子供の頃、暇に飽かせてよくやった「車のナンバープレートの数字を足して計算する」という遊びに似たようなものらしいのでこれにしました。

問題は下記のようなものです。

問題文:「四つ数字の間に「四則演算」の演算子を入れて計算した結果が、元の数字を「逆から並べた数字」になるものを求めます。」

制限は二つあります。

1. 数字は、四桁の整数である 1000 から 9999 まで。
2. 演算子は、最低一つ入れます。

何だか簡単そうにも思えましたが、手を動かそうと思うと動きません。
行き当たりばったりでいつも書いている戦法が、効きそうにないです。
やはり、最初に少しステップを踏んで考えることにしました。

 
 
 

「コピーキャット」:

問題の意味が分からなかったので、もう一度問題を読み直してみます。

「四つ数字の間に「四則演算」の演算子を入れて計算した結果が、元の数字を「逆から並べた数字」になるものを求めます。」

文言より、例を挙げて考える方が問題の意味を理解できそうです。

「1234」の場合、「1234」を反転させると「4321」。
「1234」に何個か演算子を入れて計算した結果が「4321」になれば良い筈です。

まずは足し算だけで考えてみます。

「1234」に「+」を全部に入れると「1+2+3+4」の計算結果が「10」。

「1234」に「+」を二個に入れると「1+2+34」の計算結果が「37」。
「1234」に「+」を二個に入れると「1+23+4」の計算結果が「28」。
「1234」に「+」を二個に入れると「12+3+4」の計算結果が「19」。

「1234」に「+」を一個に入れると「1+234」の計算結果が「235」。
「1234」に「+」を一個に入れると「12+34」の計算結果が「46」。
「1234」に「+」を一個に入れると「123+4」の計算結果が「127」。

この例を見返すと、四つの演算子の組み合わせを総当たりで試すことになりそうな感じがしてきました。

問題の制約から数字は「四桁」と決まっているので、入れることが出来る演算子の最大数は「三つ」と分かりました。
演算子を数字に入れる位置も分かりました。
また最低「一つ」は演算子が入るという「制限」がありますが、これは「三つ」、「二つ」、「一つ」のどれでも構わないということです。
言い換えると、演算子を入れなくても良い箇所もあるので、ここは少し工夫する必要がありそうです。

もう一つ難問があります。

演算子を入れた式をどうやって「計算」するかです。
これには「文字列を式として評価する」機能がプログラミング言語にあります。
インタプリタ言語では元々得意な分野ですので、その方向で解決したいと思います。

 
 
 

「スタイロ」:

プログラミング言語の指定はありません。
何でも書き易いので良いかなとおもいましたが、取り敢えず Python で書こうと思います。
完成後に気力が残っていればですが、別のプログラミング言語で書き直すのも勉強になりそうです。

先ほどの机上での試行をコードに書き下ろした初版を下記にしまします。

初版(ループ入れ子バージョン):

# four digits and operators
import time
start = time.time()
operator = ['+', '-', '/', '*', '']
for i in range(1000, 10000):
    ask = str(i)  # ex. '1234'
    num = []
    for j in ask: 
        num.append(str(j))  # ex. ['1', '2', '3', '4']
    temp = num.copy()
    temp.reverse() # return None
    correct = int(''.join(temp))    # ex. '4321'
    for op1 in operator:
        for op2 in operator:
            for op3 in operator:
                number = num.copy() # reset
                number.insert(1, op1)
                number.insert(3, op2)
                number.insert(5, op3)
                # ex. ['1', '+', '2', '-', '3', '*', '4']
                formula = ''.join(number)
                try:
                    answer = eval(formula)  # calculate
                    if len(formula) != 4 and correct >= 1000:
                        if correct == answer:
                            print(ask, ':', formula, '=', correct)
                except ZeroDivisionError:
                    pass
                except SyntaxError:
                    pass
end = time.time()
elapsed = end - start
endroll1 = 'elapsed time:'
endroll2 = ', And Then There Were None' # written by Agatha Christie
print(endroll1, '{0:.2f} [sec]'.format(elapsed), endroll2)

何度も試行錯誤して何とかちゃんと動きました。

難関の「文字列を式として評価する」機能は、eval() を利用することで解決出来ました。
計算式として成立しない「文法エラー」"SyntaxError" と「零割(零除算)」"ZeroDivisionError" は、例外構文で排除しました。

計算式として成立しない文法エラーの例は、「1+02-3」。
計算式として成立しない零除算の例は、「1+23/0」。

また、演算子が入らない箇所に関しては、「空文字」を演算子のリストに加えることで可能にしましたが、一個以上の演算子が入っていることを確約するために、生成した「式」の文字数をカウントして必ず一個は入る様にしています。

大した工夫していないのに何とか動いているのは、インタプリタ言語の Python のお蔭です。

ちゃんと動いたので嬉しいですが、総当たり戦という駄目なロジックなので実行には少し時間が掛かります。
結果が出るまで、もどかしいので実行時間の計測もしてみました。
ここでは、単純な経過時間を秒数で表示させています。

初版実行結果(ループ入れ子バージョン):

5931 : 5*9*31 = 1395
elapsed time: 11.03 [sec] , And Then There Were None

どうやら、11秒程度かかりました。
そして、肝心の正解は「5931」の一個だけでした。

 
 
 

「ウィジン」:

インタプリタ言語の Python のお蔭で何とか解決出来ました。
ですが、ループがネストしすぎて少し格好悪いかなとも思いました。
もうちょっとスマートに書けると思いますが、難しそうです。
それにも益して、少し実行に時間が掛かります。

「四人組」"GoF; Gang of Four" の一人「マーティン・ファウラー」"Martin Fowler" の顔が浮かびました。
すかさず「リファクタリング」"refactoring" することにしました。

リファクタリングする目的は、単にコードを整理するだけでなく、もう少し速くならないものかと想ったのです。
そこで、 Python の必殺技「内包表記」に手を出すことにしました。

リファクタリング版(内包表記バージョン):

# four digits and operators
# use list comprehension version
import time
start = time.time()
operator = ['+', '-', '/', '*', '']
for i in range(1000, 10000):
    ask = str(i)  # ex. '1234'
    num = [str(j) for j in ask] # ex. ['1', '2', '3', '4']
    temp = num.copy()
    temp.reverse() # return None
    correct = int(''.join(temp))     # ex. '4321'
    comp = [(num[0], op1, num[1], op2, num[2], op3, num[3]) for op3 in operator for op2 in operator for op1 in operator]
	# ex. [['1', '+', '2', '-', '3', '*', '4'], ...]
    for number in comp:
        formula = ''.join(number)
        try:
            answer = eval(formula)
            if len(formula) != 4 and correct >= 1000:
                if correct == answer:
                    print(ask, ':', formula, '=', correct)
        except ZeroDivisionError:
            pass
        except SyntaxError:
            pass    
end = time.time()
elapsed = end - start
endroll1 = 'elapsed time:'
endroll2 = ', And Then There Were None' # written by Agatha Christie
print(endroll1, '{0:.2f} [sec]'.format(elapsed), endroll2)

先ほどと同じように、単純な経過時間を秒数で表示させています。

リファクタリング版実行結果(内包表記バージョン):

5931 : 5*9*31 = 1395
elapsed time: 9.84 [sec] , And Then There Were None

大体ですが、一秒位速くなりました。
少しスピードアップしたのは、内包表記のお蔭だと言えます。

ですが、相当分かりづらいスクリプトになってしまいました。
それに、他にも副次的な影響が出ているものと憶測されます。
どっちが良いと言えなくて「痛し痒し」なのかもしれません。

 
 
 

「ノー・タイム・トゥ・ダイ」:

先ほど実行時間の計測には、time モジュールを使って単純な経過時間を求めています。
この方法は実行時一回だけの計測ですので、実行するタイミングによって処理速度にコンマ何秒程度の誤差が出ました。
Python ではより正確に「実行時間計測」できる timeit モジュールが用意されています。
どんどん趣旨から離れていきます(いつもの事です)が、これも試してみようと思います。

timeit モジュールを使ってスクリプト内で実行時間を計測するには、関数にする必要があります。
前述のコードを関数化することでリファクタリングしてから、timeit モジュールを採用しました。
十回実行した平均値で各々の実行時間を計測したのは下記のコードです。

実行時間計測修正コード(断片):

import timeit
def main():
    operator = ['+', '-', '/', '*', '']
        :
loop = 10
result = timeit.timeit('main()', globals=globals(), number=loop)
elapsed = result / loop
endroll1 = 'elapsed average time:'
endroll2 = ', And Then There Were None' # written by Agatha Christie
print(endroll1, '{0:.2f} [sec]'.format(elapsed), endroll2)

上記に書き換えて、二つの版で各々計測してみます。

初版実行結果(ループ入れ子バージョン):

5931 : 5*9*31 = 1395
5931 : 5*9*31 = 1395
     :
elapsed average time: 10.91 [sec] , And Then There Were None

リファクタリング版実行結果(内包表記バージョン):

5931 : 5*9*31 = 1395
5931 : 5*9*31 = 1395
     :
elapsed average time: 9.96 [sec] , And Then There Were None

たった十回だけの繰り返し計測(本当は千回くらいやったほうが良いかも)ですが、平均でも約一秒程度速くなったことを確認できました。
少しスピードアップしたのは内包表記のお蔭と言えますが、このコードには潜在的な副次効果があります。

内包表記版では、結果的に全部の組み合わせを二次元リストとしてメモリに保持しています。
ですから、ループネスト版よりもメモリ使用量が確実に増えている筈です。
データ量がそんなに大きくないのが幸いしてオンメモリで実行できている様子なので問題が出ていませんが、もしこれが数字四桁じゃなくて桁数が大きくデータが膨大な量になるとパフォーマンス劣化するだろうことが容易に予想できます。
メモリ容量が実行速度にも大きな影響が出るのは、間違い無いのは経験済みです。
課題に対して実行環境(物理マシン)との兼ね合いなどを考慮した適切なアプローチの考察が必要になります。

今回は時間切れでメモリ容量までは確認していません。
次回は、時間計測だけでなくプログラム実行の際にもう一つ課題であるメモリ使用量の確認もしてみたいと思います。
これに関しては、またいつかご報告します。

 
 
 

「ビッチズ・ブロークン・ハーツ」:

見落としていました。
眼はただの節穴でした。

速度改善について色々試した後ですが、もっと大きな改善が出来ました。
問題文をもう一度よく精査すると、大きな見落としに気が付きました。

最初に挙げた問題の例を再帰させて再考察します。

「1234」の場合、「1234」を反転させると「4321」。

「1234」に「+」を全部に入れると「1+2+3+4」の計算結果が「10」。

「1234」に「+」を二個に入れると「1+2+34」の計算結果が「37」。
「1234」に「+」を二個に入れると「1+23+4」の計算結果が「28」。
「1234」に「+」を二個に入れると「12+3+4」の計算結果が「19」。

「1234」に「+」を一個に入れると「1+234」の計算結果が「235」。
「1234」に「+」を一個に入れると「12+34」の計算結果が「46」。
「1234」に「+」を一個に入れると「123+4」の計算結果が「127」。

計算結果だけを見ると、最大で三桁です。

四桁の数字の足し算で最大となるのは、「9999」の場合です。
「9999」に「+」を一個に入れると「999+9」の計算結果が「1008」で反転させると「8001」。
この場合は四桁にはなりますが、正解ではありません。
この凡例から、足し算では問題が成立しないことが判別できます。

同様に、引き算、割り算では計算結果が足し算よりも小さな値になるはずです。
ですから、引き算、割り算でも同様に四桁の数字にならないことが推理できます。

これらを判定しなくてよいと「仮定」すると、「演算子」の指定は「掛け算」だけを考慮すれば良かったのです。

前述のコードで演算子のリストを下記のように変更しました。

#operator = ['+', '-', '/', '*', '']
operator = ['*', '']

上記で試してみます。

ループ入れ子バージョン:

5931 : 5*9*31 = 1395
5931 : 5*9*31 = 1395
     :
elapsed average time: 0.65 [sec] , And Then There Were None

前回の約十七分の一という段違いの大幅な速度改善が図れました。

速度改善という意味だけに限定したとしても、コードの小手先の「工夫」よりも、前提条件をよく咀嚼して大本である「アルゴリズム」を効率よく考えて取り組むことが大事なのだと痛感しました。
早とちりのあわて者(せっかち、いらち)なのですが、腰を据えてじっくりと「本質を見極める」ことが肝要でした。
装飾となる枝葉も大事なのですが、やはり根っこ(根本、本質)が一番重要です。

 
 
 

「エヴリシング・アイ・ウォンテッド」:

漆黒の海に静かに落ちていく。
頭上にある海面が遠のいていく。
叫ぼうとしたけど海の中では声も届かない。
朧げな月だけがゆらゆらと遥か彼方に見える。
その頼りない月光さえも細く小さくなっていく。
すべてが黒く塗り潰された闇に覆われていく。

誰も気づいてくれなかった。
誰も悲しんでくれなかった。

すべてを手に入れたんだと思ったけど、
ただの夢だった。

皆そばにいてくれると感じていたのに、
そうじゃなかった。

夢だった。
長い夢だった。
悪夢のようだった。

 
 
 

でも、夢から覚めるとあなただけが傍にいてくれた。

 
 
 

次回をお楽しみに。

 
 
 

 


 

 [IT研修]注目キーワード   Python  UiPath(RPA)  最新技術動向  Microsoft Azure  Docker  Kubernetes