第16章 なぜGPUで速くなるのか、そして次の一歩

こんにちは。ゆうせいです。

第1章から続いてきたこの連載も、最終章です。第15章までで、順方向の計算も、学習のための逆方向の計算も、その大半が行列演算であることを確認してきました。第16章では、その事実がもたらす実務的な帰結を扱います。なぜGPUという装置が、ディープラーニングに使われるのでしょうか。もともとは画像処理のための装置だったものが、なぜ機械学習の主役になったのでしょうか。答えは、行列積という計算の性質にあります。そして最後に、この連載では扱わなかった話題と、次に学ぶべきことを示して締めくくります。

行列積の並列性

各要素は独立に計算できる

第9章で確認した行列積の計算を、思い出してください。

結果のi行j列の要素は、左の行列のi行目と、右の行列のj列目の内積です。

ここで重要なのは、ある要素の計算が、他の要素の計算結果を必要としないことです。

1行1列目を計算するために、1行2列目の結果は要りません。逆も同様です。

つまり、すべての要素を、同時に計算できます。

順番に計算する必要がある処理との対比

すべての計算が並列化できるわけではありません。

たとえば、次のような処理を考えます。

a_1 = 1

a_2 = a_1 + 1

a_3 = a_2 + 1

a_2 を計算するには、a_1 の結果が必要です。a_3 を計算するには、a_2 の結果が必要です。

このような処理は、順番に実行するしかありません。

依存関係がないことの価値

行列積には、この依存関係がありません。

(1000, 1000) の行列どうしの積であれば、100万個の要素があります。この100万個を、理論上は同時に計算できます。

計算資源さえあれば、100万倍の高速化が可能ということです。

現実には、そこまでの並列度は得られませんが、この性質が高速化の余地を生んでいます。

CPUとGPUの違い

CPUの設計思想

CPUは、汎用的な処理を行うために設計されています。

少数の高性能なコアを持ちます。一般的なCPUでは、4個から64個程度です。

各コアは、複雑な処理を高速に実行できます。条件分岐、順次処理、多様な命令に対応します。

たとえるなら、少数の熟練した職人です。どんな仕事でもこなせますが、人数は限られています。

GPUの設計思想

GPUは、単純な処理を大量に並列実行するために設計されています。

多数の単純なコアを持ちます。数千個から1万個以上です。

各コアの性能は、CPUのコアより劣ります。複雑な処理は苦手です。

たとえるなら、大人数の作業員です。一人ひとりは単純な作業しかできませんが、同じ作業を同時に大量にこなせます。

なぜ画像処理から来たのか

GPUは、もともと画像処理のために開発されました。

画面上の何百万個という画素それぞれについて、色を計算する。この処理は、画素ごとに独立しています。

つまり、画像処理も、行列積と同じく、独立した計算を大量に実行するという構造を持っていたのです。

この共通性が、GPUが機械学習に転用された理由です。装置の性質と、計算の性質が、たまたま一致していました。

比較表

項目CPUGPU
コアの数4個〜64個程度数千個〜1万個以上
1コアの性能高い低い
得意な処理複雑で順次的な処理単純で並列的な処理
条件分岐得意苦手
行列積苦手得意

実際の速度差

どれくらい違うのか

行列積において、GPUはCPUの数十倍から数百倍の速度を出します。

行列が大きいほど、差が広がります。小さな行列では、データをGPUに転送する時間のほうが長くなり、かえって遅くなることもあります。

なぜ大きいほど有利なのか

GPUには、数千個のコアがあります。

小さな行列では、計算すべき要素が少ないため、多くのコアが遊んでしまいます。

大きな行列であれば、すべてのコアに仕事が行き渡ります。

バッチサイズを大きくすることが推奨されるのは、この理由によります。第9章で扱ったバッチ処理は、GPUの性能を引き出すための工夫でもあるのです。

データ転送の問題

GPUを使う際の注意点として、データ転送のコストがあります。

計算に使うデータは、CPU側のメモリからGPU側のメモリへ、転送する必要があります。

この転送には、時間がかかります。

したがって、次のような使い方は非効率です。

データを転送し、小さな計算をし、結果を戻し、また転送する。これを繰り返すと、転送時間が大半を占めてしまいます。

効率的な使い方は、データをGPUに置いたまま、多くの計算を実行することです。

計算精度を下げるという工夫

数値の表現形式

第3章で、float32が1個あたり4バイトを占めることを扱いました。

より少ないビット数で数値を表現する形式もあります。

float16は、1個あたり2バイトです。

bfloat16も、1個あたり2バイトですが、表現できる範囲が異なります。

int8は、1個あたり1バイトです。

精度を下げる利点

メモリ使用量が減ります。float32からfloat16にすれば、半分になります。

計算が速くなります。少ないビット数の演算は、高速に実行できます。

メモリからの読み込みも速くなります。データ量が少ないためです。

精度を下げる欠点

数値の精度が落ちます。細かい違いが表現できなくなります。

学習が不安定になる場合があります。特に、勾配の値が極端に小さい場合、float16では0になってしまうことがあります。

混合精度学習

実務では、混合精度学習という手法がよく使われます。

計算の大半をfloat16で行い、精度が必要な部分だけをfloat32で行う方法です。

具体的には、行列積はfloat16で、パラメータの更新はfloat32で行います。

これにより、速度とメモリを改善しつつ、学習の安定性を保てます。

実務でのメモリ管理

第14章と第15章で扱った内容を、実務の観点から整理します。

学習時に必要なメモリ

パラメータそのもの。

各パラメータに対する勾配。

最適化手法が保持する状態。Adamであれば、パラメータの2倍。

各層の中間出力。バッチサイズに比例します。

具体的な見積もり

1億2400万パラメータのモデルを、float32で学習する場合を考えます。

パラメータが約496メガバイト。

勾配が約496メガバイト。

Adamの状態が約992メガバイト。

ここまでで、約2ギガバイトです。

これに、中間出力の分が加わります。バッチサイズ32、系列長512、埋め込み768次元、12層の場合、中間出力だけで数ギガバイトになります。

メモリ不足への対処

メモリ不足のエラーが出た場合、次の順で対処するのが定石です。

第一に、バッチサイズを減らします。中間出力の量が比例して減ります。

第二に、混合精度学習を使います。メモリが約半分になります。

第三に、勾配チェックポインティングを使います。中間出力の保存を減らし、計算時間と引き換えにメモリを節約します。

第四に、モデルを複数のGPUに分割します。

いずれの対処も、第3章で学んだメモリ量の計算に基づいて判断できます。

この連載で扱わなかったこと

第1章で述べたとおり、この連載では、大学の線形代数で中心的に扱われるいくつかの話題を、意図的に省略しました。

ここで、それらが何であり、どのような場面で必要になるかを示します。

行列式

行列式は、行列が空間の体積をどれだけ変化させるかを表す値です。

第11章で、射影によって面積が0になることを扱いました。行列式が0であることが、この状況に対応します。

ディープラーニングの基本的な計算には登場しませんが、確率モデルの一部や、正規化フローと呼ばれる生成モデルの手法では必要になります。

逆行列

行列 A に対して、掛けると単位行列になる行列を、逆行列と呼びます。

AA^{-1} = I

線形方程式を解く際に使われます。

ディープラーニングでは、逆行列の計算はほとんど使われません。計算量が大きく、数値的に不安定になりやすいためです。勾配降下法という反復的な方法が、代わりに使われます。

ただし、統計モデルや、一部の最適化手法では登場します。

固有値と固有ベクトル

行列を掛けても方向が変わらないベクトルを、固有ベクトルと呼びます。その際の伸縮率が、固有値です。

A\mathbf{v} = \lambda\mathbf{v}

主成分分析という次元削減の手法で、中心的な役割を果たします。

また、学習の安定性を理論的に解析する際にも使われます。

特異値分解

任意の行列を、三つの行列の積に分解する手法です。

第12章で触れた低ランク近似は、この特異値分解に基づいています。

LoRAという、大規模モデルの効率的な微調整の手法も、関連する考え方に基づいています。

直交行列

列どうしが互いに直交し、長さが1である行列です。

第11章で扱った回転行列は、直交行列の一例です。

パラメータの初期化方法や、学習の安定化手法で使われることがあります。

次に学ぶべきこと

優先度1:微分の理解を深める

線形代数の次に必要になるのは、微分です。

第14章と第15章では、微分の詳細に立ち入りませんでした。

偏微分、連鎖律、ヤコビアンといった概念を理解すると、誤差逆伝播法の仕組みが、より正確に把握できるようになります。

優先度2:確率と統計

損失関数の設計、正則化、モデルの評価。いずれも、確率と統計の知識が土台になります。

特に、交差エントロピー誤差という代表的な損失関数は、確率の考え方なしには理解できません。

優先度3:主成分分析を通じた固有値

固有値と固有ベクトルを学ぶなら、主成分分析という具体的な応用と一緒に学ぶことを推奨します。

抽象的な定義から入るより、データの次元をどう減らすかという目的から入るほうが、理解しやすくなります。

優先度4:実装を通じた深化

最も効果的な学習方法は、小さなニューラルネットワークを、フレームワークを使わずに実装してみることです。

この連載で扱った内容だけで、全結合層のネットワークは実装できます。

順伝播、損失計算、逆伝播、パラメータ更新。この一連の流れを自分で書いてみると、理解の穴がどこにあるかが、はっきりします。

連載全体の振り返り

第1章から第16章までを、あらためて整理します。

第I部:データを数値で表す

スカラー、ベクトル、行列、テンソルという入れ物を確認しました。

そして、画像、文章、表形式データといった、まったく異なる種類のデータが、いずれも数値の並びに変換されることを見ました。この共通化が、線形代数がディープラーニングの共通言語となる出発点です。

第II部:計算の正体を知る

ベクトルの足し算とスカラー倍から始まり、内積、行列とベクトルの積、行列と行列の積へと進みました。

最も重要だったのは、ニューロン1個の計算が内積であり、層全体の計算が行列積であるという対応です。

\mathbf{y} = W\mathbf{x} + \mathbf{b}

この一行が、何百万回もの計算を表しています。

また、形を合わせる技術として、転置、reshape、ブロードキャストを扱いました。実務で最も頻繁に遭遇するエラーへの対処法です。

第III部:計算の意味を理解する

行列を掛けることが、空間の変形であることを見ました。回転、伸縮、射影。行列の各列が、基本ベクトルの行き先を表しています。

次元の拡大では情報は増えず、縮小では情報が失われます。

そして、線形変換だけを何層重ねても一層と等価になるという限界を確認し、活性化関数の必要性を理解しました。

第IV部:学習の仕組みとのつながり

勾配がベクトルであること、パラメータの更新がスカラー倍と引き算であることを見ました。

誤差逆伝播では、順伝播で使った行列の転置が現れます。順伝播で情報が散らばり、逆伝播で影響が集まってくる。この対称性が、転置として表現されます。

そして本章で、これらすべてが行列積という形に整理されているからこそ、GPUによる高速化が可能であることを確認しました。

Pythonで確認する

import numpy as np
import time

print("=== 行列積の並列性 ===")
A = np.array([[1, 2], [3, 4]])
B = np.array([[5, 6], [7, 8]])

print("結果の各要素は、独立に計算できます:")
for i in range(2):
    for j in range(2):
        val = A[i] @ B[:, j]
        print(f"  結果[{i},{j}] = Aの{i}行目 ・ Bの{j}列目 = {val}")
print("どの計算も、他の計算結果を必要としません。")

print("\n=== 依存関係がある処理との対比 ===")
print("並列化できない例(前の結果が必要):")
a = [1]
for i in range(4):
    a.append(a[-1] * 2 + 1)
    print(f"  a[{i+1}] = a[{i}] * 2 + 1 = {a[-1]}")

print("\n=== 行列サイズと計算量 ===")
print(f"{'サイズ':>12} | {'要素数':>14} | {'掛け算の回数':>16}")
print("-" * 48)
for n in [10, 100, 1000, 10000]:
    elements = n * n
    mults = n * n * n
    print(f"{n:>5} x {n:<5} | {elements:>14,} | {mults:>16,}")

print("\n=== CPUでの行列積の速度 ===")
print(f"{'サイズ':>12} | {'時間(ms)':>12} | {'GFLOPS':>10}")
print("-" * 40)
for n in [128, 256, 512, 1024, 2048]:
    A_t = np.random.randn(n, n).astype(np.float32)
    B_t = np.random.randn(n, n).astype(np.float32)
    
    A_t @ B_t  # ウォームアップ
    
    n_trials = max(1, int(1e9 / (n**3)))
    start = time.time()
    for _ in range(n_trials):
        A_t @ B_t
    elapsed = (time.time() - start) / n_trials
    
    flops = 2 * n**3
    gflops = flops / elapsed / 1e9
    print(f"{n:>5} x {n:<5} | {elapsed*1000:>12.3f} | {gflops:>10.2f}")

print("\n=== データ型による違い ===")
n = 1024
print(f"{n}x{n} の行列について:")
for dtype, name in [(np.float64, "float64"), (np.float32, "float32"), (np.float16, "float16")]:
    A_d = np.random.randn(n, n).astype(dtype)
    size_mb = A_d.nbytes / 1024 / 1024
    print(f"  {name:>8}: 1要素{A_d.itemsize}バイト, 行列全体{size_mb:>7.2f} MB")

print("\n=== 精度を下げたときの誤差 ===")
np.random.seed(0)
A32 = np.random.randn(500, 500).astype(np.float32)
B32 = np.random.randn(500, 500).astype(np.float32)
C32 = A32 @ B32

A16 = A32.astype(np.float16)
B16 = B32.astype(np.float16)
C16 = (A16 @ B16).astype(np.float32)

rel_error = np.abs(C32 - C16).mean() / np.abs(C32).mean()
print(f"float32とfloat16の相対誤差: {rel_error:.6f}")
print("多くの用途では、この程度の誤差は許容範囲です。")

print("\n=== 学習時のメモリ見積もり ===")
def estimate_memory(n_params, batch_size, seq_len, d_model, n_layers, dtype_bytes=4):
    param_gb = n_params * dtype_bytes / 1024**3
    grad_gb = param_gb
    adam_gb = param_gb * 2
    activation_gb = batch_size * seq_len * d_model * n_layers * 5 * dtype_bytes / 1024**3
    return param_gb, grad_gb, adam_gb, activation_gb

configs = [
    ("GPT-2 small相当", 124_000_000, 32, 512, 768, 12),
    ("中規模モデル", 1_500_000_000, 8, 1024, 1600, 48),
]

for name, n_params, bs, sl, d, nl in configs:
    p, g, a, act = estimate_memory(n_params, bs, sl, d, nl)
    total = p + g + a + act
    print(f"\n{name}({n_params:,}パラメータ、バッチ{bs})")
    print(f"  パラメータ:   {p:>7.2f} GB")
    print(f"  勾配:         {g:>7.2f} GB")
    print(f"  Adam状態:     {a:>7.2f} GB")
    print(f"  中間出力:     {act:>7.2f} GB")
    print(f"  合計:         {total:>7.2f} GB")

print("\n=== バッチサイズを減らす効果 ===")
n_params = 124_000_000
p = n_params * 4 / 1024**3
fixed = p * 4
print(f"固定部分(パラメータ、勾配、Adam): {fixed:.2f} GB")
print(f"\n{'バッチサイズ':>12} | {'中間出力(GB)':>14} | {'合計(GB)':>12}")
print("-" * 42)
for bs in [64, 32, 16, 8, 4]:
    act = bs * 512 * 768 * 12 * 5 * 4 / 1024**3
    print(f"{bs:>12} | {act:>14.2f} | {fixed + act:>12.2f}")

print("\n=== この連載で学んだ演算の一覧 ===")
operations = [
    ("ベクトルの足し算", "残差接続、バイアス加算、位置エンコーディング", "第5章"),
    ("スカラー倍", "学習率、勾配クリッピング、正規化", "第5章"),
    ("内積", "ニューロン1個の計算、類似度の測定", "第6章"),
    ("行列とベクトルの積", "全結合層(データ1件)", "第8章"),
    ("行列と行列の積", "全結合層(バッチ処理)", "第9章"),
    ("転置", "形合わせ、誤差逆伝播", "第10章、第15章"),
    ("外積", "重みに対する勾配", "第15章"),
    ("アダマール積", "活性化関数の微分の適用", "第15章"),
]

print(f"{'演算':>20} | {'主な用途':>40} | {'章':>12}")
print("-" * 78)
for op, use, ch in operations:
    print(f"{op:>20} | {use:>40} | {ch:>12}")

演習

演習1:並列化できるかを判断する

次の処理について、並列化できるかどうかを判断してください。

1000個の数値それぞれを2倍にする。

数列の各項が、前の項に1を足したものである数列を、1000項まで求める。

1000行1000列の行列積を計算する。

損失関数を計算するために、1000個の誤差の2乗を合計する。

演習2:メモリ量を計算する

70億パラメータのモデルについて、次の二つの場合のメモリ量を計算し、比較してください。中間出力は考慮しなくて構いません。

float32で学習する場合。パラメータ、勾配、Adamの状態を含めます。

float16で推論だけを行う場合。パラメータのみとします。

演習3:小さなネットワークを実装する

この連載の総まとめとして、フレームワークを使わずに、全結合ネットワークを実装してください。

import numpy as np

np.random.seed(0)

class SimpleNetwork:
    def __init__(self, sizes):
        self.Ws = []
        self.bs = []
        for i in range(len(sizes) - 1):
            W = np.random.randn(sizes[i+1], sizes[i]) * np.sqrt(2.0 / sizes[i])
            b = np.zeros(sizes[i+1])
            self.Ws.append(W)
            self.bs.append(b)
    
    def relu(self, x):
        return np.maximum(0, x)
    
    def forward(self, X):
        """X: (バッチサイズ, 入力次元)"""
        self.cache = [X]
        h = X
        for i, (W, b) in enumerate(zip(self.Ws, self.bs)):
            z = h @ W.T + b
            self.cache.append(z)
            h = self.relu(z) if i < len(self.Ws) - 1 else z
            self.cache.append(h)
        return h
    
    def backward(self, y_true, lr=0.01):
        batch_size = y_true.shape[0]
        y_pred = self.cache[-1]
        
        grad = 2 * (y_pred - y_true) / batch_size
        
        for i in reversed(range(len(self.Ws))):
            h_prev = self.cache[2*i]
            z = self.cache[2*i + 1]
            
            if i < len(self.Ws) - 1:
                grad = grad * (z > 0)   # ReLUの微分(アダマール積)
            
            grad_W = grad.T @ h_prev     # 外積の集まり
            grad_b = grad.sum(axis=0)
            grad = grad @ self.Ws[i]      # 転置した重みを掛ける
            
            self.Ws[i] -= lr * grad_W
            self.bs[i] -= lr * grad_b

# サイン関数を学習させる
X = np.linspace(-3, 3, 200).reshape(-1, 1)
y = np.sin(X)

net = SimpleNetwork([1, 32, 32, 1])

print("=== 学習の経過 ===")
print(f"{'エポック':>8} | {'損失':>12}")
print("-" * 24)
for epoch in range(2001):
    y_pred = net.forward(X)
    loss = ((y_pred - y) ** 2).mean()
    if epoch % 400 == 0:
        print(f"{epoch:>8} | {loss:>12.6f}")
    net.backward(y, lr=0.05)

print("\n=== 学習結果の確認 ===")
test_x = np.array([[-2.0], [-1.0], [0.0], [1.0], [2.0]])
pred = net.forward(test_x)
print(f"{'入力':>8} | {'予測':>10} | {'正解':>10} | {'誤差':>10}")
print("-" * 44)
for i in range(len(test_x)):
    true_val = np.sin(test_x[i, 0])
    print(f"{test_x[i,0]:>8.1f} | {pred[i,0]:>10.4f} | {true_val:>10.4f} | "
          f"{abs(pred[i,0]-true_val):>10.4f}")

print("\nこの連載で学んだ演算だけで、非線形な関数を学習できました。")

演習の解答例と解説

演習1の解答は、次のとおりです。

1000個の数値をそれぞれ2倍にする処理は、並列化できます。各要素の計算が独立しているためです。

数列を求める処理は、並列化できません。各項が前の項に依存しているためです。

行列積は、並列化できます。結果の各要素が独立に計算できるためです。

1000個の誤差の2乗の合計は、部分的に並列化できます。2乗の計算はすべて並列に行えます。合計する部分は、木構造で段階的に足し合わせることで、ある程度並列化できます。この手法をリダクションと呼びます。

演習2の解答は、次のとおりです。

float32で学習する場合。

パラメータは次のようになります。

7 \times 10^9 \times 4 = 28 \times 10^9

約28ギガバイト、約26.1ギビバイトです。

勾配も同じく約26.1ギビバイトです。

Adamの状態は、その2倍で約52.2ギビバイトです。

合計は、約104.4ギビバイトです。

float16で推論のみの場合。

7 \times 10^9 \times 2 = 14 \times 10^9

約13ギビバイトです。

差は、約8倍です。

一般的なGPUの搭載メモリは24ギガバイトから80ギガバイト程度ですから、推論なら1台で可能ですが、学習には複数台が必要になります。

演習3では、この連載で学んだ演算だけを使って、実際に動くニューラルネットワークが実装できることが確認できます。

コードの中に登場する演算は、行列積、転置、アダマール積、ベクトルの足し算、スカラー倍だけです。すべて、この連載で扱ったものです。

サイン関数という非線形な関数を、ReLUという単純な活性化関数と行列演算だけで近似できていることも、確認してください。第13章で扱った、折れ線で曲線を近似するという話が、実際に機能しています。

講師向けの補足

最終章では、連載全体の振り返りに時間を割いてください。

第1章で示した線形代数の三つの役割を再掲し、それぞれがどの章で扱われたかを対応づけると、全体像が整理されます。

演習3の実装は、必ず実施してもらってください。フレームワークなしでネットワークが動くという体験は、達成感が大きく、理解の総点検にもなります。

また、次に何を学ぶべきかを明示することが重要です。学習を終えた受講者が、次の一歩を踏み出せるよう、具体的な方向を示してください。

固有値や特異値分解を扱わなかった理由も、あらためて説明してください。必要ないから省いたのではなく、基礎理解の段階では不要だから後回しにしたという位置づけを、明確にすることが大切です。

理解度を確認する問い

第16章の内容を、次の問いで確認してみてください。

行列積が並列化できる理由を説明してください。

CPUとGPUの設計思想の違いを、コアの数と性能という観点から説明してください。

バッチサイズを大きくすることが、GPUの性能を引き出すうえで重要なのはなぜでしょうか。

混合精度学習とは何であり、どのような利点があるでしょうか。

学習時にメモリ不足になった場合、どのような対処が考えられるでしょうか。

まとめ

行列積では、結果の各要素が互いに独立に計算できます。この依存関係のなさが、並列処理を可能にしています。

CPUは少数の高性能なコアを持ち、複雑で順次的な処理を得意とします。GPUは多数の単純なコアを持ち、単純な処理を大量に並列実行することを得意とします。行列積は後者に適した構造を持つため、GPUによる大幅な高速化が可能になります。

GPUがもともと画像処理のために開発された装置であることは、偶然ではありません。画素ごとの独立した計算という構造が、行列積と共通していたのです。

行列が大きいほど、GPUの多数のコアに仕事が行き渡り、効率が上がります。バッチサイズを大きくすることが推奨されるのは、この理由によります。

計算精度を下げることで、メモリと計算時間をさらに節約できます。実務では、精度が必要な部分だけを高精度で計算する混合精度学習が広く使われています。

学習時のメモリ不足に対しては、バッチサイズの削減、混合精度学習、勾配チェックポインティング、複数GPUへの分割といった対処があります。いずれも、第3章で学んだメモリ量の計算に基づいて判断できます。

この連載では、行列式、逆行列、固有値、特異値分解といった話題を扱いませんでした。これらは、主成分分析、モデルの圧縮、確率モデルといった、より進んだ領域で必要になります。

次の学習としては、微分の理解を深めること、確率と統計を学ぶこと、そして小さなネットワークを自分で実装してみることを推奨します。

最後に

第1章で、次のように述べました。

ディープラーニングの計算は、その大半が行列の掛け算と足し算である。線形代数は、それを記述し、実行するための言語である。

16章を経た今、この一文の意味が、具体的な内容を伴って理解できているはずです。

ニューロン1個の計算は内積でした。層全体の計算は行列積でした。学習の計算も、転置した行列を掛けることでした。そして、それらすべてが行列積という形に整理されているからこそ、GPUによる高速化が成立していました。

線形代数は、ディープラーニングの周辺にある補助的な知識ではありません。ディープラーニングそのものが、線形代数で書かれています。

長い連載におつきあいいただき、ありがとうございました。次は、ぜひ自分の手でコードを書いてみてください。この連載で学んだ内容が、画面の中で実際に動くことを確認したとき、理解は本物になります。

セイ・コンサルティング・グループでは新人エンジニア研修のアシスタント講師を募集しています。

投稿者プロフィール

山崎講師
山崎講師代表取締役
セイ・コンサルティング・グループ株式会社代表取締役。
岐阜県出身。
海外放浪の末、2000年創業、2004年会社設立。
IT企業向け人材育成研修歴業界歴20年以上。
すべての無駄を省いた費用対効果の高い「筋肉質」な研修を提供します!
この記事に間違い等ありましたらぜひお知らせください。

学生時代は趣味と実益を兼ねてリゾートバイトにいそしむ。長野県白馬村に始まり、志賀高原でのスキーインストラクター、沖縄石垣島、北海道トマム。高じてオーストラリアのゴールドコーストでツアーガイドなど。現在は野菜作りにはまっている。