画像セグメンテーション用のアルゴリズム、u-netについて簡単に説明する

きっかけ

kaggleであるコンペに参加しようとしてkernelを見たところ、みんな「u-net」ってアルゴリズムの話をしていた。聞いたことがなかったし全然分からなかったので調べてみた。

論文の著者が5分で簡単に説明している動画があるので、その内容をまとめる。
5 Minute Teaser Presentation of the U-net: Convolutional Networks for Biomedical Image Segmentation - YouTube

5 Minute Teaser Presentation of the U-net: Convolutional Networks for Biomedical Image Segmentation
youtube動画に対して、私が英語の文字起こしをして、さらに日本語訳の字幕を追加した。しかし現時点ではどちらも表示されていなくて悲しい……(誰かが字幕の審査をしてOKを出さないと表示されない。どなたか文字起こし・字幕の審査にOKを出してくれるとありがたい)

※この記事を書くにあたり、u-netの論文は読んでいません。ご注意ください。

概要、何に使うのか

u-netは画像のセグメンテーションのためのアルゴリズム。2015年に発表された。

手法のキモはどこか

u-netのアーキテクチャ(構成図)はこちら。
f:id:soratokimitonoaidani:20180924002248p:plain
これがU字型に見えるからu-netって名前なんですね、

3×3のkernelを用いて畳み込みをして、Max-poolingで縮小、という処理はCNNで一般的だ。その後、2×2のkernelを用いて拡大をして、元のサイズに戻す。
畳み込み→拡大という流れはAuto-Encoderと似ているのだが、縮小段階の途中の特徴量マップを、拡大段階の学習で再度使用するというのが最大の特徴である。

具体的に説明しよう。
図の上部に長い右向きの灰色矢印がある。矢印左側では、縮小段階の途中の568×568の特徴量マップが64枚ある。
矢印右側では、アップサンプリングしてきた392×392の特徴量マップが64枚ある(64という数字は直接登場しないが、結合結果が128枚であることから64枚と考えられる)。
縮小途中の64枚+アップサンプリングしてきた64枚 の合計128枚を結合(concatenate)したのちに入力として、次の特徴量マップ(390×390サイズ、64枚)を作る。
矢印の左右で画像サイズが異なるので、568×568から中央392×392だけを切り出して(crop)使用している。

通常のCNNは畳み込みでパディングをしているが、u-netの畳み込み時には、実行しない。
このため画像サイズが処理を経るごとに小さくなっていく。
(なんでパディングをしないのかは謎。kaggle上での実装を見てみると普通にパディングしてる例もある。)

先行研究と比べてどこがすごいか?

学習に使う画像の数が少なくても良いらしい。
あと学習および学習後の適用が高速である。(u-netは全結合の層が全く無いので、パラメータ数が小さい。それが速い理由だろうか?)

既存手法に比べて画像セグメンテーションの精度が良い。

参考

論文著者のサイト:
U-Net: Convolutional Networks for Biomedical Image Segmentation
機械学習の論文を日本語でまとめているarXivTimes上での説明:
U-Net: Convolutional Networks for Biomedical Image Segmentation - Issue #294 - arXivTimes/arXivTimes - GitHub

pythonのdillでは正規表現matchオブジェクトが保存できない

dillというのは、pickleの強化版のようなツールだ。現在使っている変数をまとめて外部のファイルに保存できる。
(バイナリにして保存することを、pickle化、シリアライズ、直列化などと呼ぶらしい)

dillについての情報はネットにあまり書いていない。下記の記事がとても参考になった。
Pythonですべての変数を保存するにはPickleよりdillが便利 - Qiita

余談ですが、「dill pickle」はピクルスの一種を指しているらしく、この語句で検索してもpythonのライブラリは出てきません。検索ワードに「python」を加えましょう。

dill pickle
《料理》ディル・ピクルス◆キュウリをディル(dill)の葉と種と共に塩漬けした物。
dill pickleの意味・用例|英辞郎 on the WEB:アルク

簡単な使い方

公式ドキュメントに使い方が載っていないので、ほぼ上記Qiitaそのままですが。

# dill_test_a.py
a = 123
b = 456
c = b - a
string = "Hello World"

import dill
dill.dump_session("session.pkl")

最後の2行で、現在の変数をすべて"session.pkl"というファイルに保存している。

# dill_test_b.py
import dill
dill.load_session("session.pkl")

print(a)
print(b)
print(c)
print(string)

'''出力:
123
456
333
Hello World
'''

保存した"session.pkl"から変数を復元し、その値を表示している。

pickleでは保存できないが、dillで保存できる例

上の例では単純な数値や文字列だけを保存(シリアライズ、直列化)した。
しかし、pythonには色々なオブジェクトがあり、pickleでは保存できないものもある。
今回の主題ではないし、私がきちんと理解していないので詳細は省くが、lambda関数、入れ子になった関数などがこれに該当する。dillではpickleを拡張子、lambda関数、入れ子になった関数なども保存できる。

公式の説明では、
12.1. pickle ? Python オブジェクトの直列化 - Python 3.6.5 ドキュメント の中の『12.1.4. pickle 化、非 pickle 化できるもの』
dillのreadme
を参照。

pickleオブジェクトがpickle化できるものの中で比較的、制限されているからです。つまり入れ子関数やlambda、スライスや、その他いろいろなものを処理できません。これらのオブジェクトを直接pickle化したいケースは、それほどはないかもしれませんが、pickle化したい他の物の中に、そういったオブジェクトが出てくることは、かなり一般的です。そのため、pickle化が失敗する原因となるのです。
私が選ぶ2015年の”新しい”Pythonモジュール トップ5 | POSTD

一部、pickle化できない種類のオブジェクトがある。
よく引っかかる例はlambda関数や、openで開いたファイルハンドラで、これが保存するオブジェクトのどこか1箇所にでも使われていると、pickle.dump()がエラーを出す。
pickle [いかたこのたこつぼ]

正規表現のマッチングオブジェクトが入っているとエラーになる

本題に入る。
色々なオブジェクトを保存できるdillだが、正規表現のmatchオブジェクトは保存できない。

import re
result = re.search("lar", "regular expression")
print(result)
print(type(result))

'''出力:
<_sre.SRE_Match object; span=(4, 7), match='lar'>
<class '_sre.SRE_Match'>
'''

"result"というmatchオブジェクトを作った。ではこれをdillで保存してみよう。

dill.dump_session("session2.pkl")
---------------------------------------------------------------------------
TypeError                                 Traceback (most recent call last)
<ipython-input-5-07b38aa0950a> in <module>()
----> 1 dill.dump_session("session2.pkl")

D:\Code\Anaconda3\lib\site-packages\dill\dill.py in dump_session(filename, main, byref)
    331         pickler._recurse = False # disable pickling recursion for globals
    332         pickler._session = True  # is best indicator of when pickling a session
--> 333         pickler.dump(main)
    334     finally:
    335         f.close()

(長いので中略)

D:\Code\Anaconda3\lib\pickle.py in save(self, obj, save_persistent_id)
    494             reduce = getattr(obj, "__reduce_ex__", None)
    495             if reduce is not None:
--> 496                 rv = reduce(self.proto)
    497             else:
    498                 reduce = getattr(obj, "__reduce__", None)

TypeError: can't pickle _sre.SRE_Match objects

ダメだ。matchオブジェクトは保存できない。


解決方法

今回は、「matchオブジェクトも保存する」のではなく、「matchオブジェクトは保存できなくても良いから、その他のオブジェクトをまとめて保存する」ことを目指す。
つまり解決と言っているのは、具体的には「matchオブジェクト以外を保存する」ことである。

dump_session()関数に「指定した変数は除外する」という引数でも無いかなぁと思ったが、
公式ドキュメントを見てもそういう引数はない。
dill module documentation - dill 0.2.9.dev0 documentation

したがって、変数を削除してからdillを実行することにする。
変数の削除はdel [変数名]で実行できる。

import re
result = re.search("lar", "regular expression")
print(result)
print(type(result))

'''出力:
<_sre.SRE_Match object; span=(4, 7), match='lar'>
<class '_sre.SRE_Match'>
'''

del result
dill.dump_session("session2.pkl")

でresult以外の変数を保存できた。
保存したファイルを使用して変数を表示することもできた。(ソースコードは最初の例のファイル名を変えただけなので特に貼らない)

それでは。

Windows+Anaconda で環境切り替えしようとししてつまづいた2つのポイント

「scikit-learnとTensorFlowによる実践機械学習」の本を読み進めている。
TensorFlowのパートに入ったので、TensorFlowの環境を通常と別に構築しようとしたのでまとめておく
(なお最終的には失敗している。)

環境:
Windows 7 64bit
python --version

  • > Python 3.6.6 :: Anaconda 4.3.1 (64-bit)

Anacondaは環境管理もできる(のでvirtualenvを使う必要はない)

参考書では通常のpythonをインストールし、pipでパッケージ管理をしている。そして、隔離された環境を作成するためにvirtualenvを使うことを推奨している。
参考書に合わせてvirtualenvを使おうと思っていたが、調べてみると、Anacondaってパッケージの管理だけじゃなくて、環境の管理もできるということを知った。

condaは仮想環境管理にも使えます。: virtualenv/venvの代わり
仮想環境下でcondaやpipでパッケージをインストールできます。
condaで作る仮想環境はpythonバージョン違いまで吸収できるため、virtualenvの上位互換と言えます。
事実、anacondaでvirtualenvを使おうとすると、condaで環境を作るように警告が出ます。(virtualenvが作れはします。)
データサイエンティストを目指す人のpython環境構築 2016 - Qiita

「Condaの迷信と誤解」というタイトルの記事にはこう書かれている。

Reality: You actually can install (some) conda packages within a virtualenv, but better is to use Conda's own environment manager: it is fully-compatible with pip and has several advantages over virtualenv.
拙訳:実際には、virtualenvを通じてcondaのパッケージをインストールすることはできる。しかし、より良いのは、Conda事態の環境管理を使用することである。これはpipと完全に互換性があり、virtualenvと比べていくつかの利点がある。

Conda: Myths and Misconceptions | Pythonic Perambulations

なお、Condaで独立した環境を構築するためのコマンドに関しては、以下を参照。
Managing environments - Conda documentation


Powershellで切り替えが上手く行かない

PowerShellの上でactivateを実行すると、特にエラーなどは発生せず、正常に実行されたように見えるが、
実際には環境が切り替わっていないという不思議な事態が起きる。

解決策は
PowerShellでAnacondaの仮想環境をactivateするメモ - Qiita
に書いてあるとおり。以下のように打つとactivateが反映される。

conda install -n root -c pscondaenvs pscondaenvs

また、activateの際にエラーメッセージが表示され、実行できなかった。
activateでは「.ps1」 という拡張子のファイル(PowerShellスクリプト)を実行するが、デフォルトではスクリプトの実行は禁止されているからである。
管理者権限でPowerShellを立ち上げて、以下のように打つと「.ps1」拡張子のファイルを実行できる。

Set-ExecutionPolicy RemoteSigned

主成分分析を使って、DDRのレベル18の譜面の傾向を分析してみた

主成分分析を使って、DDRのレベル18の譜面の傾向を分析してみた

前書き

音ゲーのスコアを主成分分析して、プレイヤーや曲の傾向が求められるのではないか? と思ったので試してみた。
いま自分が一番やっている音ゲーはSOUND VOLTEXなのだが、みんなのスコアデータを取得してくる方法がないため、DDRで分析をやってみた。

注意事項:私はデータ分析についてそれほど熟練してるわけではない。しかも、DDRについてもそれほど熟練してるわけではない。(ごく少しのレベル18をギリギリクリアできる程度) 譜面についておかしな分析をしていたら、ご容赦ください。

データ収集

まずデータを収集する必要がある。すなわち、色々なプレイヤーのスコアを収集する。

有志が作成したSkill Attackというサイトがあり、そこからデータをスクレイピングした。
Skill Attackでは、個人がDDRのスコアデータを登録し、また他の人のスコアを閲覧することができる。
なお、登録しているスコアは譜面ごとの過去最高スコアである。
まずSkill Attack Difficulty Ranking (Level 18)のページを起点とした。
ここには各プレイヤーのレベル18のスコア合計・平均があるので、そこから容易にプレイ済み曲数を計算可能である。
ある程度上手い人、かつある程度曲を埋めている(=スコアを登録している)人を今回の分析対象とした。具体的には、レベル18のスコア平均値が95万点以上(満点は100万点)、プレイ済み曲数が30曲以上(曲数は全部で35曲)とした。この条件を満たした対象プレイヤーは94人である。
対象プレイヤーのスコア一覧をスクレイピングした。なお、スコアを登録していない曲については欠損値になることに注意。

データの収集(スクレイピング)は2018年8月1日の0:00頃に実施した。

データの一部は次のようになる。
f:id:soratokimitonoaidani:20180817002800j:plain
さて主成分分析……と行きたいところだが、前処理として2つの工程を実行する必要がある。
それは「不適当な曲の削除」と「欠損値の補完」である。

前処理(不適当な曲の削除)

まず「不適当な曲の削除」について。曲の特性(出現条件と出現タイミング)を考えて、データ分析するには不適当な曲があるので、その曲のデータを削除する必要がある。

「ACE FOR ACES 鬼」はレベル18なので今回の集計対象曲である。しかし、この譜面がプレイ可能になったのは7月26日と、データ収集をした僅か6日前であり、スコアデータを登録しているプレイヤーがほとんどいない。欠損値が大半であるため、この曲のデータは分析に使えないと判断し削除する。

レベル18の中で、「ACE FOR ACES 鬼」の次に新しい譜面は「ENDYMION 激」である。これは同日の7月26日から通常選曲できるようになった。それまでは4回ミスしたら即終了という条件下でプレイしなければならなかった。
この厳しい条件だとスコアは下がってしまうのか。それとも熟練のDDRプレイヤーたちはこの条件でも他のレベル18と変わらないスコアを出すのだろうか。
これを確認するため、箱ひげ図を用いて各曲のスコア分布状況を可視化する。

f:id:soratokimitonoaidani:20180817002735p:plain

明らかにENDYIONは他の曲と比べて傾向が違い、下方の外れ値も多く存在する(つまりスコアが極端に低いプレイヤーがいる)。 ENDYIONを分析に含めてしまうと、この曲の特殊条件の影響を強く受けてしまうため、分析が上手くできないと考えられる。したがって、「ENDYMION 激」も除外する。

(これによって 本当は2曲を除外したあとのスコア平均値が95万点以上、としたほうが合理的ではある。
しかしながら、この方法は集計対象プレイヤーがすぐには求まらないので複雑である。
今回は、ある程度の人数が集まればよしと思ってたので、ここについてはこだわらずに次に行く)

2曲を削除した後で箱ひげ図を再度描画してみた。

f:id:soratokimitonoaidani:20180817002741p:plain

Pluto The First 鬼」では明らかに外れ値が発生している。特に80万点を割っている人が2人いるのが気になる。しかし、この曲は最近出たというわけではない。除外せずに分析を続行する。

ちなみに、スコアが一番高い傾向にある曲は「Come to Life 鬼」だと分かる。
譜面動画見たけど結構キツくないか? 延々と16分滝を踏まされるから、クリアがギリギリの人の目線で見ると決して一番簡単には思えない。しかし、「リズムが全体的に分かりやすい」「全体BPMが180と低め」「低速部分に難解な配置がない」「同時押し絡みの強い配置がない」と考えていくと……上手い人にとってはスコアを出しやすいんだろう。多分。


前処理(欠損値の補完)

次に、欠損値の補完をする。
あるプレイヤーがある譜面をプレイしていない場合はデータがない欠損値になっている。主成分分析をするためには、これを何らかの値で埋める必要がある。
今回は単純に「その曲の平均値」で埋めることにした。
つまり、例えばあるプレイヤーAの「Elemental Creation 鬼」のスコアが無かった場合、「Elemental Creation 鬼」をプレーしているプレイヤーのスコア平均値で代用するのだ。

ただしこの手法では、そのプレイヤーAが上手いか下手かを無視している(プレイヤーAが上手ければ、スコア平均値よりももっと高いスコアを出すと推測される)。その分だけ分析の精度は悪くなる。
単に平均値を使うよりももっと複雑な処理が必要になってくる。
スコアについての何らかのモデルを考え、それに当てはめて欠損したスコアを推測するのが良いだろう。
より分析精度を上げるためにはより適切な欠損値補完の手法が必要であろう。

もともと30曲以上のスコアを登録している人に限定したのは、欠損値が数曲であればその影響も軽微であるためだ。
(35曲のうち10曲しかプレイしていない人のデータの場合、残り25曲の欠損値を埋める必要がある。この場合はデータの中身が欠損値の補完方法に大きく左右されてしまう。したがって、元々のデータの情報が少なすぎて上手く分析できないと考えた。)


主成分分析(その1)

以上の前処理をした後に、主成分分析を実行する。
下記は第1主成分のうち、それぞれの曲の成分を示したものである(値の順にソートしている)。
f:id:soratokimitonoaidani:20180818005809p:plain
第1主成分は全ての曲についてマイナス成分だ。
主成分は「ベクトルの方向」を示しているので、全体をマイナス1倍しても問題はなく、そうすると全ての曲の成分がプラスである。
どの曲についても、曲のスコアが増えれば第1主成分も増える。……すなわち、第1主成分はプレイヤーの上手さ、「実力」の成分であると解釈して問題ないだろう。


第2主成分は以下の通りである。
f:id:soratokimitonoaidani:20180818005816p:plain
絶対値が最大の曲は「Pluto The First 鬼」で、その値は-0.867である。
……他の曲と比べて値の絶対値が大きすぎる。

主成分は長さ(要素の二乗の和)が1になるようにスケーリングされている。
その中で、(-0.867)^2 = 0.751 が「Pluto The First 鬼」の成分である。……ほとんどPluto The Firstじゃねぇか!!!

一体何が起こっているのか。
それは第1・第2主成分を散布図でプロットすると判明する。
f:id:soratokimitonoaidani:20180818010221p:plain
先ほど見たように、「Pluto The First 鬼」……そろそろフルで呼ぶのが面倒になってきたので略称の「プルファス」を使う……プルファスのスコアが極端に低いプレイヤーが2人いた。
第2主成分のうちプルファスの成分は-0.867で、負である。すなわち、プルファスのスコアが低ければ第2主成分は大きくなる。
プルファスのスコアが低く、第2主成分が極端に大きい点が2つ存在する。

プルファスのスコアが低い人に合わせて主成分ベクトルを作ってしまう事態が起きているのだ。
データ分析の用語を使うと、主成分が外れ値に強く影響されている。

プルファスのスコアと第2主成分の相関係数は実に-0.752と、強い相関を示している。ほとんど「Pluto The First 鬼」のスコアだけで第2主成分が決まっているのだ。

……それでは反対側の成分が大きい曲はどうなっているだろうか。一定の傾向が出ているだろうか。

New Century 0.164
Cosy Catastrophe 0.150
888 0.133
POSSESSION 0.131
New Decade 0.130

あんまり共通点が感じられない……

第3を飛ばして、第4主成分を見てみると、なんとなく傾向があるように見える。

f:id:soratokimitonoaidani:20180818005813p:plain
★マークを付けたのは、BPM400の8分よりも速い滝を踏まなければいけない曲である。表の下のほうに固まっている。
この条件に該当する曲は

  • EGOISM 440
  • Over The Period
  • 888
  • MAX.(period)
  • Elemental Creation
  • Pluto The First
  • Astrogazer
  • 嘆きの樹

である。(正確にはNeutrino、IXも該当するが、アクセントとしてごく一部に入っているだけなので除外しておく。)

こうしてみると最近の曲が多い。もしパラハデが最近登場していたら、絶対ラストの滝に三連符(12分)が混ざってたよな……

ともかく、第4主成分のうちこれらの曲の要素がマイナスになっているので、これらの曲のスコアが高いと第4主成分が下がる。したがって、「第4主成分が低いプレイヤーは、テンポの速い曲が得意」という傾向が導けた。第4主成分には何とか意味づけができそうだ。


主成分分析(その2)

最初の主成分分析ではあまり上手い分析ができなかった。これは全てプルファスが悪い。
あんなメチャクチャなショックアローが入っていて、あんなわけの分からない停止がある譜面が悪い。
……というわけで、プルファスの存在を除外して再度分析してみよう。

第1~第4主成分は以下の通りである。(左から第1、第2の順番)
f:id:soratokimitonoaidani:20180818005805p:plain

第1主成分は先ほどと同様、全ての成分が負であり、「プレイヤーの実力」を示している。

ここまでは同じだが、第2主成分は違ってくるはずで、一定の傾向がわかる……

絶対値が最大の曲は「Neutrino 鬼」で、その値は-0.678である。
……他の曲と比べて値の絶対値が大きすぎる。

主成分は長さ(要素の二乗の和)が1になるようにスケーリングされている。
その中で、(-0.678)^2 = 0.460 が「Neutrino 鬼」の成分である。……大きな部分を「Neutrino 鬼」が占めている。「Neutrino 鬼」のスコアと第2主成分の値の相関係数は-0.615で、強い相関を示している。

……はい。さっきのプルファスがNeutrinoに変わったような結果である。プルファスほど極端ではないけれど。

こうなる仕組み(原因)も同じで、Neutrinoにも下方の外れ値がいくつか存在したからである。外れ値の外れ方がプルファスよりも弱いので、傾向もプルファスほど極端ではない、という状況である。


なおも見ていくと、不思議なことに気がついた。
主成分のうち、絶対値の大きい要素には特定の曲が出現しやすい。このあたりの曲だ。
Cosy Catastrophe
Neutrino
DEAD END (GROOVE RADAR Special)
Tohoku EVOLVED

逆に、Come To Lifeや冥などは絶対値が小さい。
この理由は考えてみれば至極当然で、Cosy Catastropheなどの曲はスコアのばらつきが大きいからである。

それを揃え、データのばらつきを一定にするのがデータの正規化であり、分析前に実施しておく必要がある。今回は「どうせどれも100万点満点の曲のスコアなんだから同じようなもんだろう」と考えてしまって正規化をしなかったが、それが裏目に出たかっこうである。


課題:さらなる分析へ向けて

今回の分析では曲やプレイヤーの傾向を示すことが目的であったが、その目的は達成されていない。
現状では3つの課題があると考える。

課題1:外れ値の除外をしていないせいで、外れ値の影響を受けやすい

先ほどはプルファスのデータを削除した。すなわち曲(=特徴量)の方を削除していたが、外れ値の除去ではサンプルを除外するほうが一般的だ。極端に周りから離れたスコアを出しているプレイヤー(=サンプル、インスタンス)を除外したほうが良さそうだ。
最初にACE FOR ACESとENDYMIONという曲を削除した(これには正当な理由があるのだが)ので、それに引きづられて曲データを削除しようとしてしまった。

機械学習/データ分析では、その手法によって「外れ値に対して安定している・安定していない」という差が合る。
今回の主成分分析はその性質上、外れ値にとても弱い。外れ値のサンプルが1つだけでも、そこに適合させようとして主成分は変化してしまうので、これは上手く除外しなければならない。

課題2:データの正規化が必要

今の状態では、Come To Lifeや冥のデータが殆ど無視されている。これは曲ごとのスコアのばらつきに差があるためなので、分散を揃える必要がある。

課題3:欠損値の補完手法に改善の余地がある

1つ目の課題よりは重要度が低いと考える。先述の通り、単に中央値で埋めるよりももっと工夫をして欠損値を埋めたほうが、分析精度が上がるはずである。

……こうしてまとめてみると、全部データの前処理じゃないか! よく言われている「データ分析の大半は前処理だよ」を身をもって思い知った。次は前処理をちゃんとやって有意義な結果を導出したい。

matplotlibの日本語文字化けが直らない→seabornの設定も必要だった


実行環境

windows 7 (64bit)
Python 3.6.0 :: Anaconda 4.3.1 (64-bit)
matplotlib.__version__ # -> '2.0.0'
seaborn.__version__ # ->'0.7.1'

一部のファイルでのみ、日本語フォントの設定が反映されない

Jupyter Notebookの上でグラフを描画したが、日本語が文字化けしていた。
「matplotlib 日本語 文字化け」で検索すると、いくつかのサイトがほとんど同じ手順を紹介している。そのとおりに設定した。しかし、色々やってみてもなぜか設定が反映されず、グラフの日本語は四角い記号(いわゆる豆腐)に文字化けしたままだった。

ところが、検証用に新たにJupyter Notebookを作って実行してみたところ

import matplotlib.pyplot as plt
%matplotlib inline 
plt.figure()
plt.xlabel('豆腐 - tofu')
plt.title('豆腐 - TOFU')

結果:
f:id:soratokimitonoaidani:20180812232037p:plain

正しく日本語が表示されている。不思議に思って設定を表示してみると以下の結果になった。

import matplotlib
matplotlib.matplotlib_fname() #設定ファイルの場所を表示する'D:\\Code\\Anaconda3\\lib\\site-packages\\matplotlib\\mpl-data\\matplotlibrc'


print(matplotlib.rcParams['font.family']) #使用フォントを表示する
→ ['IPAexGothic']

設定変更のために色々試していたので、Anacondaをインストールしたフォルダの中のmatplotlibrcを直接書き換えている。(本当は、自分のぶんのmatplotlibrcを別途作って、それを読み込ませるのが良い)
ともかく、IPAフォントを正しく読み込んで設定できているのが分かる。

ところが、元々の(日本語が文字化けしたままの)Jupyter Notebookで同じように設定を表示したところ……

import matplotlib
matplotlib.matplotlib_fname()
→ 'D:\\Code\\Anaconda3\\lib\\site-packages\\matplotlib\\mpl-data\\matplotlibrc'


print(matplotlib.rcParams['font.family'])
['sans-serif']

あれれ。
全く同じ設定ファイルを読み込んでいるにもかかわらず、使用フォントが違っている。
フォント設定が ['sans-serif'] になっていて、IPAフォントを読み込めていないから、文字化けが起きているんだ。
何でこんなことが起きているんだ?

キャッシュ削除、ファイル名変更→効果なし

前からこの作業ファイルを使っていたから、キャッシュが設定ファイルを無視して古い設定を読み込んでいるんだろうか……?
それならば、フォント関連のキャッシュファイルを削除することで解決しそうだ。

C:\Users\[ユーザ名]\.matplotlib の下のfontList.py3k.cache というファイルを削除し、全てのNotebookを一旦閉じて、Powershell上でjupyterを停止・再起動してみた。
しかし、状況は全く変わらない。

次に、全てのNotebookを一旦閉じて、Powershell上でjupyterを停止して、問題のファイル(.ipynb)を名前変更。
しかし、状況は全く変わらない。

キャッシュ周りの問題ではなさそうだ。

こうなったらファイルの中身が問題だろうな……と、元々のファイルを少しずつコピーしながらフォント設定を確認し、原因を切り分けていった。
その結果がこちら。

import matplotlib
print(matplotlib.rcParams['font.family'])
→ ['IPAexGothic']

import seaborn as sns

import matplotlib
print(matplotlib.rcParams['font.family'])
→ ['sans-serif']

seabornを読み込んだところでmatplotlibの設定フォントが勝手に変わっている! 何これ!

一部のファイルだけ直らない理由もわかった。
新たに作ったファイルでは、seabornをimportせずにmatplotlibだけimportしてからグラフを描いたから、日本語が正常に表示されたんだ。


seabornのフォント設定

seaborn font familyで検索したら出てきた。

(pythonのインストールフォルダ)\Lib\site-packages\seaborn\rcmod.py を開いて以下の通り修正する。

def set(context="notebook", style="darkgrid", palette="deep",
        font="sans-serif", font_scale=1, color_codes=False, rc=None):

def set(context="notebook", style="darkgrid", palette="deep",
        font="IPAexGothic", font_scale=1, color_codes=False, rc=None):

に変更する。
この修正を実行すると、matplotlibおよびseabornで描いたグラフの日本語が正しく表示された。


結論

matplotlibだけではなく、seabornを使う場合には別途設定が必要です(seaborn用の設定ファイルを変更する必要がある)。


参考:
matplotlibで描画したグラフの文字化けを解消する - Qiita
【matplotlib】日本語の設定 - keisukeのブログ
【Seaborn】日本語を表示する (フォントを変更する) - Qiita