本章では、文部科学省によって作成された、高等学校情報科「情報Ⅱ」教員研修用教材(以後「文科省教材」と呼ぶ)の内容に沿って、データサイエンスの基本について学んでいきます。「文部科学省ウェブサイト利用規約」に則り、 「高等学校情報科 情報Ⅱ 教員研修用教材(本編) 第3章 情報とデータサイエンス 」(文部科学省)で扱っている内容を一部加工して作成しております。
特に、前章までにご紹介した、Pythonプログラミングや音声グラフの技術を活かして、実際にデータサイエンスの基本を実践できるように工夫しています。 基本とその実践方法を身につけることができれば、本コンテンツ以外にも膨大に存在する、オンライン上の学習リソースなどを活用して、自ら学び続けることが出来るようになるでしょう。
データサイエンスでは、情報の科学的な見方・考え方を駆使して問題を明確にし、分析方針を立て、様々なデータに対する整理、整形、分析を行います。そして、その結果を考察する活動を通して、人工知能による画像認識、自動翻訳など、機械学習を活用した様々な製品やサービスが開発されたり、新たな知見が生み出されたりしていることを理解するようになるでしょう。 さらに、不確実な事象を予測するなどの問題発見・解決を行うために、データの収集、整理、整形、モデル化、可視化、分析、評価、実行、効果検証など、各過程における様々な知識や技能を身に付け、データに基づいて科学的に物事を捉えたり、問題解決に取り組む力が養われます。
データサイエンスの一連の流れについて、細かな解説は程々にして、 まずは一通り手を動かして実践することを優先します。 音声グラフライブラリの使い所について、感覚を掴むことが出来れば、 インターネット上で公開されている様々なコンテンツを活用して、理解を深めていくことが出来ると思います。
以下の項目を順番に学習することにより、データサイエンスに関する全体像を掴んでいきましょう。
まずは、このあとに呼び出される外部ライブラリのインポートを行います。 Google Colabratoryを使っていれば、ほとんどのライブラリはインストール済みで、 以下のようにimportするだけで使えるようになるはずです。
import numpy as np
import pandas as pd
from sklearn.preprocessing import LabelEncoder, StandardScaler
from sklearn.linear_model import LogisticRegression
from sklearn.model_selection import train_test_split
from sklearn.metrics import classification_report
from tensorflow import keras
一方、音声グラフライブラリについては、まだインストールしていない場合は、 pipコマンドを使ってインストールしてから、インポートしましょう。
!pip install -q audio-plot-lib
import audio_plot_lib as apl
また、データサイエンスでは、機械学習する際の初期値などに、乱数が多用されます。 そのままだと、実験をするごとに結果が変わってしまい、問題解決の妨げになったりすることも多いため、乱数のシード(種)を以下のように固定しておくこともよく行われます。
np.random.seed(0) # 0は好きな数値に変更しても良い
今回の例題として、最近のデータサイエンス学習でよく使われるようになりつつある、 ペンギンデータセット を取り上げます。 データの量や質が適切で、とても扱いやすいため、人気のあるデータセットです。
中身は、次のような3種類のペンギンについて、それぞれの体重や嘴の長さといった、特徴的なデータがCSVにまとめられています。
これらのペンギンは、日本の水族館でも見ることの出来る人気のあるペンギン達です。 データセットの名前にPalmerとついているのですが、南極地域のパーマー半島における調査データが元になっているようです。
早速、データセットをpandasのdataframe形式として読み込んでみましょう。
penguin_dataset_url = "https://raw.githubusercontent.com/allisonhorst/palmerpenguins/master/inst/extdata/penguins_raw.csv"
df_penguin = pd.read_csv(penguin_dataset_url)
また、分かりやすくするために、各データの名称やペンギンの名称を日本語に変換しておきましょう。
df_penguin = df_penguin.rename(columns={
"studyName": "学名",
"Sample Number": "サンプル番号",
"Species": "種名",
"Region": "地域",
"Island": "島名",
"Stage": "発達段階",
"Individual ID": "個体識別番号",
"Clutch Completion": "成熟済み",
"Date Egg": "生年月日",
"Culmen Length (mm)": "上嘴の長さ(mm)",
"Culmen Depth (mm)": "嘴の縦幅(mm)",
"Flipper Length (mm)": "水かきの長さ(mm)",
"Body Mass (g)": "体重(g)",
"Sex": "性別",
"Delta 15 N (o/oo)": "窒素同位体比",
"Delta 13 C (o/oo)": "安定同位体比",
"Comments": "備考",
})
df_penguin = df_penguin.replace("Adelie Penguin (Pygoscelis adeliae)", "アデリーペンギン")
df_penguin = df_penguin.replace("Chinstrap penguin (Pygoscelis antarctica)", "ヒゲペンギン")
df_penguin = df_penguin.replace("Gentoo penguin (Pygoscelis papua)", "ジェンツーペンギン")
先頭の3つだけ、データの内容を確認してみましょう。
print(df_penguin.head(3))
実行結果は以下のような感じになると思います。
学名 | サンプル番号 | 種名 | 地域 | 島名 | 発達段階 | 個体識別番号 | 成熟済み | 生年月日 | 上嘴の長さ(mm) | 嘴の縦幅(mm) | 水かきの長さ(mm) | 体重(g) | 性別 | 窒素同位体比 | 安定同位体比 | 備考 | |
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
0 | PAL0708 | 1 | アデリーペンギン | Anvers | Torgersen | Adult, 1 Egg Stage | N1A1 | Yes | 2007-11-11 | 39.1 | 18.7 | 181 | 3750 | MALE | nan | nan | Not enough blood for isotopes. |
1 | PAL0708 | 2 | アデリーペンギン | Anvers | Torgersen | Adult, 1 Egg Stage | N1A2 | Yes | 2007-11-11 | 39.5 | 17.4 | 186 | 3800 | FEMALE | 8.94956 | -24.6945 | nan |
2 | PAL0708 | 3 | アデリーペンギン | Anvers | Torgersen | Adult, 1 Egg Stage | N2A1 | Yes | 2007-11-16 | 40.3 | 18 | 195 | 3250 | FEMALE | 8.36821 | -25.333 | nan |
データの量を確認してみましょう。 shapeで出力されるのは、(データ数, 特徴数)となります。
print(df_penguin.shape)
(344, 17)
それぞれのペンギンについて、何個ずつのデータが含まれているかも確認してみましょう。
print(df_penguin["種名"].value_counts())
種名 | |
---|---|
アデリーペンギン | 152 |
ジェンツーペンギン | 124 |
ヒゲペンギン | 68 |
ペンギンデータセットは、比較的整理された使いやすいデータセットではあるのですが、 それでも、一部不要なデータなどが含まれています。 データサイエンスの世界では、「gabage in, gabage out」というのがよく言われるように、良い分析結果を出すためには、そもそものデータの質を高めておくことが重要です。 実際に、様々なデータを扱うことが増えてくると、こういった処理の手間はもっと増えると思いますので、その重要性を心に留めておいてください。
さて、今回は欠損値だけでも見ていきましょう。
print(df_penguin.isnull().sum())
0 | |
---|---|
学名 | 0 |
サンプル番号 | 0 |
種名 | 0 |
地域 | 0 |
島名 | 0 |
発達段階 | 0 |
個体識別番号 | 0 |
成熟済み | 0 |
生年月日 | 0 |
上嘴の長さ(mm) | 2 |
嘴の縦幅(mm) | 2 |
水かきの長さ(mm) | 2 |
体重(g) | 2 |
性別 | 11 |
窒素同位体比 | 14 |
安定同位体比 | 13 |
備考 | 290 |
嘴の長さや体重といった特徴は、重要な特徴だと思われるため、それらの値をもたないデータは、利用すべきではないと考えます。 以下のように、欠損値を除去したデータフレームに用意し直してみましょう。
df_penguin = df_penguin.dropna(subset=["上嘴の長さ(mm)", "嘴の縦幅(mm)", "水かきの長さ(mm)", "性別"])
print(df_penguin.isnull().sum())
print(df_penguin["種名"].value_counts())
0 | |
---|---|
学名 | 0 |
サンプル番号 | 0 |
種名 | 0 |
地域 | 0 |
島名 | 0 |
発達段階 | 0 |
個体識別番号 | 0 |
成熟済み | 0 |
生年月日 | 0 |
上嘴の長さ(mm) | 0 |
嘴の縦幅(mm) | 0 |
水かきの長さ(mm) | 0 |
体重(g) | 0 |
性別 | 0 |
窒素同位体比 | 9 |
安定同位体比 | 8 |
備考 | 290 |
種名 | |
---|---|
アデリーペンギン | 146 |
ジェンツーペンギン | 119 |
ヒゲペンギン | 68 |
一部の特徴は、キーワードなどで表されており、こういった値は数値に変換したほうが、その後の処理がしやすくなります。 性別や種名の名称を、新たに番号で表す特徴に変換してみましょう。
df_penguin.loc[:, "性別番号"] = LabelEncoder().fit_transform(df_penguin["性別"])
print(df_penguin[["性別", "性別番号"]].head(3))
性別 | 性別番号 | |
---|---|---|
0 | MALE | 1 |
1 | FEMALE | 0 |
2 | FEMALE | 0 |
df_penguin.loc[:, "種名番号"] = LabelEncoder().fit_transform(df_penguin["種名"])
print(df_penguin[["種名", "種名番号"]].head(3))
種名 | 種名番号 | |
---|---|---|
0 | アデリーペンギン | 0 |
1 | アデリーペンギン | 0 |
2 | アデリーペンギン | 0 |
それぞれの特徴に相関がないか、音声グラフを使って確かめてみましょう。 まずは、水かきの長さと体重の相関を確かめます。 水かきが長ければ、その分体重も重たくなるような気がしますが、いかがでしょうか。
x = df_penguin["水かきの長さ(mm)"].values
y = df_penguin["体重(g)"].values
apl.interactive.plot(y, x)
次に、嘴の長さや幅のデータを確認し、嘴の形状に何かしらの特徴がないか確認してみましょう。 嘴が長いからといって、縦幅も広いというわけではなさそうです。 そもそも途中、くちばしの長さが40mmくらいから音の傾向が変わり、低い音、つまりくちばしの幅が狭いデータが聞こえてきます。 どうやら細長い嘴のデータと、そうでもないデータがあり、これらは種別に関係がありそうです。
x = df_penguin["上嘴の長さ(mm)"].values
y = df_penguin["嘴の縦幅(mm)"].values
apl.interactive.plot(y, x)
各データをペンギンの種類ごとにラベル付けをして、音声グラフの確認をしてみましょう。 この時点で、嘴の特徴から、各ペンギンをある程度判別できそうなことが分かれば、分析の方針を立てる事ができます。
まずは、各ペンギンのラベル番号を確認しておきましょう。
for label in range(3):
shumei = df_penguin[df_penguin["種名番号"] == label]["種名"].values[0]
print(f"{label}は{shumei}")
0はアデリーペンギン
1はジェンツーペンギン
2はヒゲペンギン
ラベル番号を頭に入れて、音声グラフの確認をしてみましょう。
x = df_penguin["上嘴の長さ(mm)"].values
y = df_penguin["嘴の縦幅(mm)"].values
label = df_penguin["種名番号"].values
apl.interactive.plot(y, x, label)
print(df_penguin[["水かきの長さ(mm)", "体重(g)", "上嘴の長さ(mm)", "嘴の縦幅(mm)", "水かきの長さ(mm)"]].describe())
水かきの長さ(mm) | 体重(g) | 上嘴の長さ(mm) | 嘴の縦幅(mm) | 水かきの長さ(mm) | |
---|---|---|---|---|---|
count | 333 | 333 | 333 | 333 | 333 |
mean | 200.967 | 4207.06 | 43.9928 | 17.1649 | 200.967 |
std | 14.0158 | 805.216 | 5.46867 | 1.96924 | 14.0158 |
min | 172 | 2700 | 32.1 | 13.1 | 172 |
25% | 190 | 3550 | 39.5 | 15.6 | 190 |
50% | 197 | 4050 | 44.5 | 17.3 | 197 |
75% | 213 | 4775 | 48.6 | 18.7 | 213 |
max | 231 | 6300 | 59.6 | 21.5 | 231 |
全ての特徴(次元とも呼ばれる)の組み合わせに対して、細かく関係性を見ていくのは不可能です。 そこで今回は、次元削減手法の一つであるPCAを使ってみます。 PCA(主成分分析)とは、相関の強い特徴を統合して、新たな合成特徴を作成する手法です。 次元削減(特徴量の数を減らすこと)により、データの分析がしやすくなったり、後述するような機械学習を行う際に、学習に必要なデータが少なく済むなどのメリットがあります。
よく使われる機械学習ライブラリであるscikit learnから、PCAをインポートしましょう。
from sklearn.decomposition import PCA
いくつか有用そうな特徴を組み合わせて、3つの合成特徴を作成します。
data = df_penguin[["水かきの長さ(mm)", "体重(g)", "上嘴の長さ(mm)", "嘴の縦幅(mm)", "水かきの長さ(mm)", "性別番号"]].values
penguins_pca = PCA(n_components=3).fit_transform(data)
penguins_pca.shape
(333, 3)
合成特徴のデータを音声グラフで確認してみましょう。
x = penguins_pca[:, 0]
y = penguins_pca[:, 1]
label = df_penguin["種名番号"].values
apl.interactive.plot(y, x, label)
x = penguins_pca[:, 2]
y = penguins_pca[:, 1]
label = df_penguin["種名番号"].values
apl.interactive.plot(y, x, label)
教師あり学習は、入力データとそれに対応する出力データ(ラベル)のペアから学習を行い、新たな入力データに対する出力を予測するタスクです。ここでは、ペンギンのデータの特徴量を入力とし、ペンギンの種名を出力として識別するようなロジスティック回帰モデルを学習します。
ロジスティック回帰とは、機械学習や統計学でよく用いられる教師あり学習の手法の一つです。その名前から連想されるかもしれませんが、ロジスティック回帰は分類問題、特に二項分類問題に特化したモデルです。
この手法の特徴は、特徴量(入力データ)と目標変数(出力データ)の間の関係を表現するのに、ロジスティック関数(またはシグモイド関数)を用いる点です。この関数は、出力を0から1の範囲に収めるため、確率として解釈することができます。
例えば、スパムメールか否かを予測する問題であれば、ロジスティック回帰はメールの内容から特徴量を抽出し、それらがスパムである確率を出力します。
ただし、ロジスティック回帰は基本的に線形の関係性しか表現できません。そのため、データの関係性が複雑で非線形の場合には、他の手法の利用を検討することが重要です。
まず、特徴量としてPCAで次元削減したデータをX
に、目的変数として種名の番号をy
に格納します。そして、データセットを訓練データとテストデータに分割します。
X = penguins_pca[:, :3]
y = df_penguin["種名番号"].values
X_train, X_test, y_train, y_test = train_test_split(X, y, train_size=0.5)
次に、ロジスティック回帰のモデルを定義し、訓練データで学習を行います。学習したモデルを用いて、テストデータの予測を行います。
model = LogisticRegression(max_iter=1000)
model.fit(X_train, y_train)
y_pred = model.predict(X_test)
最後に、モデルの性能を評価します。ここでは、精度、再現率、F1スコアを計算し、それぞれのペンギンの種類について表示しています。これらの指標は、モデルがどの程度正確に予測できているかを示しています。
evaluation = classification_report(y_test, y_pred, target_names=["アデリーペンギン", "ジェンツーペンギン", "ヒゲペンギン"], output_dict=True)
print(pd.DataFrame(evaluation))
アデリーペンギン | ジェンツーペンギン | ヒゲペンギン | accuracy | macro avg | weighted avg | |
---|---|---|---|---|---|---|
precision | 0.925 | 1 | 1 | 0.964072 | 0.975 | 0.966766 |
recall | 1 | 0.983333 | 0.848485 | 0.964072 | 0.943939 | 0.964072 |
f1-score | 0.961039 | 0.991597 | 0.918033 | 0.964072 | 0.956889 | 0.96352 |
support | 74 | 60 | 33 | 0.964072 | 167 | 167 |
ロジスティック回帰の代わりにディープニューラルネットワークを用いてみましょう。ディープニューラルネットワークとは、人間の脳が情報を処理する方式を模倣した機械学習の一種です。脳のニューロンが情報を伝達する仕組みを、コンピュータ上で再現することを目指しています。それらは”層”と呼ばれるネットワークの集合体で、データを次々と伝達し、処理することで複雑な問題を解決します。
ロジスティック回帰と異なり、ディープニューラルネットワークは一般的に多層の構造を持ちます。各層は独自の役割を果たし、合わせてより高度な認識・理解を可能にします。これにより、単純な線形の問題だけでなく、非線形の複雑な問題も扱うことができます。
ただし、ディープニューラルネットワークの訓練は、計算リソースや時間が多く必要となります。そのため、問題の複雑さやデータ量を考慮して、適切なモデルを選ぶことが大切です。
まず、ペンギンのデータセットから特徴量として使用するカラムを選択し、それらをXに格納します。次に、目的変数(ペンギンの種類)をone-hotエンコーディングし、yに格納します。そして、データセットを訓練データとテストデータに分割します。
X = df_penguin[["水かきの長さ(mm)", "体重(g)", "上嘴の長さ(mm)", "嘴の縦幅(mm)", "水かきの長さ(mm)", "性別番号"]].values
y = pd.get_dummies(df_penguin["種名番号"]) # one hot vector
X_train, X_test, y_train, y_test = train_test_split(X, y, train_size=0.5)
ニューラルネットワークは、特徴量のスケールが異なると学習がうまく進まないことがあります。そのため、データを標準化(平均0、標準偏差1になるように変換)します。ここでは、StandardScalerを使用しています。
scaler = StandardScaler()
scaler.fit(X_train)
X_train_scaled = scaler.transform(X_train)
X_test_scaled = scaler.transform(X_test)
print(f"元の体重データの平均:{X_train[:, 1].mean().round()}, 標準偏差:{X_train[:, 1].std().round()}")
print(f"標準化後の体重データの平均:{X_train_scaled[:, 1].mean().round()}, 標準偏差:{X_train_scaled[:, 1].std().round()}")
元の体重データの平均:4242.0, 標準偏差:810.0
標準化後の体重データの平均:-0.0, 標準偏差:1.0
次に、ニューラルネットワークのモデルを定義します。ここでは、入力層、2つの隠れ層、出力層からなるモデルを作成しています。隠れ層の活性化関数にはReLUを、出力層の活性化関数にはsoftmaxを使用しています。
inputs = keras.Input(shape=X_train_scaled.shape[1])
hidden_layer = keras.layers.Dense(20, activation="relu")(inputs)
hidden_layer2 = keras.layers.Dense(10, activation="relu")(hidden_layer)
output_layer = keras.layers.Dense(3, activation="softmax")(hidden_layer2)
model = keras.Model(inputs=inputs, outputs=output_layer)
定義したモデルを訓練データで学習します。最適化アルゴリズムにはAdamを、損失関数にはカテゴリカルクロスエントロピーを使用しています。
optimizer = keras.optimizers.Adam()
loss = keras.losses.CategoricalCrossentropy()
model.compile(optimizer, loss)
history = model.fit(X_train_scaled, y_train, epochs=200, verbose=0, validation_data=(X_test_scaled, y_test))
学習の進行を視覚化するために、訓練データと検証データの損失をプロットします。
plot_x = np.arange(len(history.history["loss"])).tolist() * 2
plot_y = history.history["loss"] + history.history["val_loss"]
plot_label = [0] * len(history.history["loss"]) + [1] * len(history.history["val_loss"])
apl.interactive.plot(plot_y, plot_x, plot_label)
学習したモデルを使ってテストデータの予測を行います。
model_output = model.predict(X_test_scaled)
y_pred = pd.DataFrame(model_output, columns=y_test.columns, index=y_test.index)
予測結果を具体的なペンギンのデータとともに表示します。
index = 10
print(f"{index}番目のデータ")
print(f"水かきの長さ:{X_test[index, 0]:.1f}mm")
print(f"体重:{X_test[index, 1]:.1f}g")
print(f"上嘴の長さ:{X_test[index, 2]:.1f}mm")
print(f"嘴の縦幅:{X_test[index, 3]:.1f}mm")
print(f"水かきの長さ:{X_test[index, 4]:.1f}mm")
print(f"性別番号:{X_test[index, 5]}")
print(f"アデリーペンギンである確率:{(y_pred.values[index, 0] * 100):.1f}%")
print(f"ジェンツーペンギンである確率:{(y_pred.values[index, 1] * 100):.1f}%")
print(f"ヒゲペンギンである確率:{(y_pred.values[index, 2] * 100):.1f}%")
print(f"正解:{['アデリーペンギン', 'ジェンツーペンギン', 'ヒゲペンギン'][y_test.idxmax(axis='columns').values[index]]}")
10番目のデータ
水かきの長さ:230.0mm
体重:5700.0g
上嘴の長さ:50.0mm
嘴の縦幅:16.3mm
水かきの長さ:230.0mm
性別番号:1.0
アデリーペンギンである確率:0.0%
ジェンツーペンギンである確率:100.0%
ヒゲペンギンである確率:0.0%
正解:ジェンツーペンギン
最後に、モデルの性能を評価します。ここでは、精度、再現率、F1スコアを計算し、それぞれのペンギンの種類について表示しています。この例では、すべての指標が1.0となっており、モデルが完全に正確に予測できていることを示しています。ただし、実際の問題では、このように完全な予測ができることは稀であり、一般的にはこれらの指標を用いてモデルの性能を評価し、必要に応じてモデルの調整を行います。
以上が、この記事で行っているニューラルネットワークの基本的な流れです。データの準備、前処理、モデルの定義、学習、予測、評価というステップを通じて、ニューラルネットワークを用いた機械学習の基本的な手順を理解することができます。
y_label_pred = y_pred.idxmax(axis="columns").values # one hot vector -> label
y_label_test = y_test.idxmax(axis="columns").values
evaluation = classification_report(y_label_test, y_label_pred, target_names=["アデリーペンギン", "ジェンツーペンギン", "ヒゲペンギン"], output_dict=True)
print(pd.DataFrame(evaluation))
アデリーペンギン | ジェンツーペンギン | ヒゲペンギン | accuracy | macro avg | weighted avg | |
---|---|---|---|---|---|---|
precision | 1 | 1 | 1 | 1 | 1 | 1 |
recall | 1 | 1 | 1 | 1 | 1 | 1 |
f1-score | 1 | 1 | 1 | 1 | 1 | 1 |
support | 74 | 59 | 34 | 1 | 167 | 167 |
教師なし学習は、入力データのみから学習を行い、データの構造やパターンを発見するタスクです。 出力データ(ラベル)が与えられないため、新種の可能性を見つけたり、予めラベル付けできない場合などに用いられます。
ここでは、k-meansクラスタリングを用いてペンギンのデータを3つのクラスタに分けています。k-meansクラスタリングは、データをk個のクラスタに分ける方法で、各クラスタの中心(セントロイド)と各データ点との距離の総和を最小にするようにクラスタリングを行います。
from sklearn.cluster import KMeans
predicted_labels = KMeans(n_clusters=3).fit_predict(penguins_pca)
最後に、PCAで次元削減したデータをプロットし、k-meansクラスタリングによって割り当てられたクラスタを色で表示しています。 これにより、データがどのようにクラスタリングされたかを視覚的に確認することができます。
x = penguins_pca[:, 0]
y = penguins_pca[:, 1]
label = predicted_labels
apl.interactive.plot(y, x, label)
ここまで、ほんの一例ではありますが、データサイエンスの一連の流れについて、少しでもイメージをつかむことが出来ましたでしょうか? 情報を科学的に理解し、問題解決に役立てる能力は、これからの時代にとって不可欠です。 私たちの目的は、このデータサイエンスの全体像を掴み、それぞれのステップ、すなわちデータの準備から特徴量エンジニアリング、教師あり学習、ニューラルネットワーク、教師なし学習までを理解し、適用する能力を身に付けることです。 ここで学んだ知識を活用して、広がるデータの海を探索し、新たな発見をする力を養いましょう。 これらの学びが皆さんが直面する未来の問題を解決するための力強いツールとなることでしょう。データサイエンスの旅における皆さんの成功を願っています。